From b11122f23b73602fb5498424ed5417c8cc7e0a9e Mon Sep 17 00:00:00 2001 From: Ariel Weisberg Date: Thu, 9 Oct 2025 10:19:28 -0700 Subject: [PATCH] SERIAL read/write support for Witnesses and Mutation Tracking Patch by Ariel Weisberg; Reviewed by Aleksey Yeschenko for CASSANDRA-20953 --- .../apache/cassandra/concurrent/Stage.java | 3 +- .../config/CassandraRelevantProperties.java | 1 + .../org/apache/cassandra/config/Config.java | 1 + .../cassandra/config/DatabaseDescriptor.java | 12 + .../ConstraintViolationException.java | 7 + .../InvalidConstraintDefinitionException.java | 7 + .../cql3/statements/BatchStatement.java | 12 +- .../statements/BatchUpdatesCollector.java | 12 +- .../cql3/statements/CQL3CasRequest.java | 458 ++++--- .../statements/ModificationStatement.java | 82 +- ...eTableSinglePartitionUpdatesCollector.java | 9 +- .../SingleTableUpdatesCollector.java | 11 +- .../cql3/statements/TransactionStatement.java | 7 +- .../cql3/statements/UpdatesCollector.java | 17 +- .../schema/CreateIndexStatement.java | 3 - .../EmbeddableSinglePartitionReadCommand.java | 120 ++ .../apache/cassandra/db/IReadResponse.java | 104 ++ .../db/KeyspaceNotDefinedException.java | 7 + .../org/apache/cassandra/db/Mutation.java | 21 +- .../db/MutationExceededMaxSizeException.java | 35 + .../org/apache/cassandra/db/ReadKind.java | 83 ++ .../org/apache/cassandra/db/ReadResponse.java | 9 +- .../db/SinglePartitionReadCommand.java | 12 +- .../apache/cassandra/db/SystemKeyspace.java | 29 +- .../org/apache/cassandra/db/WriteType.java | 73 +- .../GuardrailViolatedException.java | 14 +- .../db/partitions/PartitionUpdate.java | 9 +- .../exceptions/AlreadyExistsException.java | 31 + .../exceptions/AuthenticationException.java | 6 + .../exceptions/CDCWriteException.java | 6 + .../exceptions/CasWriteTimeoutException.java | 37 +- .../CasWriteUnknownResultException.java | 35 + .../exceptions/CassandraException.java | 183 ++- .../exceptions/CassandraExceptionCode.java | 96 ++ .../exceptions/ConfigurationException.java | 6 + .../exceptions/ExceptionSerializer.java | 2 +- .../FunctionExecutionException.java | 43 + .../exceptions/InvalidRequestException.java | 6 + .../exceptions/InvalidRoutingException.java | 8 +- .../exceptions/IsBootstrappingException.java | 6 + .../OperationExecutionException.java | 20 + .../exceptions/OverloadedException.java | 6 + .../OversizedCQLMessageException.java | 6 + .../PreparedQueryNotFoundException.java | 31 + ...eferencesTooManyIndexesAbortException.java | 38 + .../exceptions/ReadFailureException.java | 89 ++ .../exceptions/ReadSizeAbortException.java | 14 + .../exceptions/ReadTimeoutException.java | 38 + .../exceptions/RequestFailureException.java | 2 +- .../exceptions/RequestTimeoutException.java | 2 +- .../cassandra/exceptions/SyntaxException.java | 6 + .../exceptions/TombstoneAbortException.java | 38 + .../exceptions/TruncateException.java | 6 + .../exceptions/UnauthorizedException.java | 6 + .../exceptions/UnavailableException.java | 35 + .../exceptions/WriteFailureException.java | 57 + .../exceptions/WriteTimeoutException.java | 38 + .../io/AsymmetricUnversionedSerializer.java | 4 + .../io/IVersionedAsymmetricSerializer.java | 4 + .../cassandra/io/IVersionedSerializer.java | 4 + .../cassandra/io/UnversionedSerializer.java | 5 + .../cassandra/locator/EndpointsForToken.java | 40 + .../cassandra/locator/ReplicaPlans.java | 2 - .../org/apache/cassandra/net/Message.java | 15 + src/java/org/apache/cassandra/net/Verb.java | 25 + .../cassandra/replication/MutationId.java | 24 + .../replication/TrackedWriteRequest.java | 3 + .../apache/cassandra/service/CASRequest.java | 61 - .../apache/cassandra/service/ClientState.java | 22 + .../cassandra/service/StorageProxy.java | 670 +++++++++- .../service/TrackedWriteResponseHandler.java | 14 +- .../AccordReadExhaustedException.java | 37 + .../AccordReadPreemptedException.java | 37 + .../AccordWriteExhaustedException.java | 34 + .../AccordWritePreemptedException.java | 34 + .../accord/txn/TxnReferenceOperations.java | 51 +- .../service/accord/txn/TxnWrite.java | 92 +- .../service/paxos/CasForwardHandler.java | 124 ++ .../service/paxos/CasForwardRequest.java | 140 +++ .../service/paxos/CasForwardResponse.java | 182 +++ .../cassandra/service/paxos/Commit.java | 227 +++- .../paxos/ConsensusReadForwardHandler.java | 122 ++ .../paxos/ConsensusReadForwardRequest.java | 77 ++ .../apache/cassandra/service/paxos/Paxos.java | 75 +- .../paxos/Paxos2CommitForwardHandler.java | 132 ++ .../paxos/Paxos2CommitForwardRequest.java | 124 ++ .../cassandra/service/paxos/PaxosCommit.java | 350 +++++- .../service/paxos/PaxosCommitAndPrepare.java | 74 +- .../paxos/PaxosCommitForwardHandler.java | 89 ++ .../paxos/PaxosCommitForwardRequest.java | 88 ++ .../cassandra/service/paxos/PaxosPrepare.java | 317 ++++- .../service/paxos/PaxosPrepareRefresh.java | 201 ++- .../cassandra/service/paxos/PaxosPropose.java | 14 +- .../cassandra/service/paxos/PaxosRepair.java | 4 +- .../service/paxos/PaxosRequestCallback.java | 63 +- .../cassandra/service/paxos/PaxosState.java | 50 +- .../paxos/PrepareRefreshForwardHandler.java | 184 +++ .../paxos/PrepareRefreshForwardRequest.java | 90 ++ .../paxos/PrepareRefreshForwardResponse.java | 72 ++ .../service/paxos/PrepareResponse.java | 4 +- .../service/paxos/uncommitted/PaxosRows.java | 23 +- .../paxos/v1/AbstractPaxosVerbHandler.java | 8 +- .../reads/tracked/TrackedDataResponse.java | 10 +- .../service/reads/tracked/TrackedRead.java | 53 +- .../reads/tracked/TrackedSummaryResponse.java | 12 +- .../triggers/TriggerDisabledException.java | 7 + .../utils/CollectionSerializers.java | 24 + .../distributed/test/PaxosRepair2Test.java | 2 +- ....java => ShortReadProtectionTestBase.java} | 25 +- ...kedReplicationShortReadProtectionTest.java | 37 + .../test/TransientRangeMovement2Test.java | 3 + .../test/TransientRangeMovementTest.java | 5 +- ...kedReplicationShortReadProtectionTest.java | 37 + .../log/MetadataChangeSimulationTest.java | 1 + .../test/log/NTSSimulationTest.java | 1 + .../test/log/OperationalEquivalenceTest.java | 1 + .../log/SimpleStrategySimulationTest.java | 1 + .../MutationTrackingCasForwardingTest.java | 273 ++++ .../MutationTrackingCommitAndPrepareTest.java | 298 +++++ .../test/tracking/MutationTrackingTest.java | 72 +- .../harry/model/TokenPlacementModel.java | 7 +- .../simulator/ClusterSimulation.java | 25 + .../cassandra/simulator/SimulationRunner.java | 11 + .../cassandra/simulator/SimulatorUtils.java | 1 + .../simulator/cluster/KeyspaceActions.java | 12 +- .../simulator/cluster/ReplicationConfig.java | 154 +++ ...bstractPairOfSequencesPaxosSimulation.java | 8 +- .../paxos/AccordClusterSimulation.java | 2 +- .../cassandra/simulator/paxos/Ballots.java | 4 +- .../PairOfSequencesAccordSimulation.java | 7 +- .../paxos/PairOfSequencesPaxosSimulation.java | 7 +- .../paxos/PaxosClusterSimulation.java | 2 +- .../paxos/PaxosSimulationRunner.java | 2 - .../test/ShortPaxosSimulationTest.java | 3 + .../ShortPaxosTrackingSimulationTest.java | 165 +++ .../ShortPaxosWitnessesSimulationTest.java | 165 +++ .../CQL3CasRequestSerializationTest.java | 523 ++++++++ .../cassandra/db/CleanupTransientTest.java | 11 +- .../db/IReadResponseSerializerTest.java | 358 ++++++ ...olumnTypeSpecificValueThresholdTester.java | 20 +- .../GuardrailColumnBlobValueSizeTest.java | 20 +- .../GuardrailColumnValueSizeTest.java | 20 +- .../CassandraExceptionSerializationTest.java | 1094 +++++++++++++++++ .../service/paxos/CasForwardingTest.java | 196 +++ .../paxos/ConsensusReadForwardingTest.java | 56 + .../service/paxos/PaxosStateTest.java | 2 +- .../paxos/uncommitted/PaxosRowsTest.java | 4 +- .../uncommitted/PaxosStateTrackerTest.java | 2 +- 148 files changed, 9111 insertions(+), 699 deletions(-) create mode 100644 src/java/org/apache/cassandra/db/EmbeddableSinglePartitionReadCommand.java create mode 100644 src/java/org/apache/cassandra/db/IReadResponse.java create mode 100644 src/java/org/apache/cassandra/db/ReadKind.java create mode 100644 src/java/org/apache/cassandra/exceptions/CassandraExceptionCode.java delete mode 100644 src/java/org/apache/cassandra/service/CASRequest.java create mode 100644 src/java/org/apache/cassandra/service/paxos/CasForwardHandler.java create mode 100644 src/java/org/apache/cassandra/service/paxos/CasForwardRequest.java create mode 100644 src/java/org/apache/cassandra/service/paxos/CasForwardResponse.java create mode 100644 src/java/org/apache/cassandra/service/paxos/ConsensusReadForwardHandler.java create mode 100644 src/java/org/apache/cassandra/service/paxos/ConsensusReadForwardRequest.java create mode 100644 src/java/org/apache/cassandra/service/paxos/Paxos2CommitForwardHandler.java create mode 100644 src/java/org/apache/cassandra/service/paxos/Paxos2CommitForwardRequest.java create mode 100644 src/java/org/apache/cassandra/service/paxos/PaxosCommitForwardHandler.java create mode 100644 src/java/org/apache/cassandra/service/paxos/PaxosCommitForwardRequest.java create mode 100644 src/java/org/apache/cassandra/service/paxos/PrepareRefreshForwardHandler.java create mode 100644 src/java/org/apache/cassandra/service/paxos/PrepareRefreshForwardRequest.java create mode 100644 src/java/org/apache/cassandra/service/paxos/PrepareRefreshForwardResponse.java rename test/distributed/org/apache/cassandra/distributed/test/{ShortReadProtectionTest.java => ShortReadProtectionTestBase.java} (97%) create mode 100644 test/distributed/org/apache/cassandra/distributed/test/TrackedReplicationShortReadProtectionTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/UntrackedReplicationShortReadProtectionTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/tracking/MutationTrackingCasForwardingTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/tracking/MutationTrackingCommitAndPrepareTest.java create mode 100644 test/simulator/main/org/apache/cassandra/simulator/cluster/ReplicationConfig.java create mode 100644 test/simulator/test/org/apache/cassandra/simulator/test/ShortPaxosTrackingSimulationTest.java create mode 100644 test/simulator/test/org/apache/cassandra/simulator/test/ShortPaxosWitnessesSimulationTest.java create mode 100644 test/unit/org/apache/cassandra/cql3/statements/CQL3CasRequestSerializationTest.java create mode 100644 test/unit/org/apache/cassandra/db/IReadResponseSerializerTest.java create mode 100644 test/unit/org/apache/cassandra/exceptions/CassandraExceptionSerializationTest.java create mode 100644 test/unit/org/apache/cassandra/service/paxos/CasForwardingTest.java create mode 100644 test/unit/org/apache/cassandra/service/paxos/ConsensusReadForwardingTest.java diff --git a/src/java/org/apache/cassandra/concurrent/Stage.java b/src/java/org/apache/cassandra/concurrent/Stage.java index 557c1ca0a4..f8f41eec0c 100644 --- a/src/java/org/apache/cassandra/concurrent/Stage.java +++ b/src/java/org/apache/cassandra/concurrent/Stage.java @@ -47,7 +47,8 @@ public enum Stage MUTATION (true, "MutationStage", "request", DatabaseDescriptor::getConcurrentWriters, DatabaseDescriptor::setConcurrentWriters, Stage::multiThreadedLowSignalStage), COUNTER_MUTATION (true, "CounterMutationStage", "request", DatabaseDescriptor::getConcurrentCounterWriters, DatabaseDescriptor::setConcurrentCounterWriters, Stage::multiThreadedLowSignalStage), VIEW_MUTATION (true, "ViewMutationStage", "request", DatabaseDescriptor::getConcurrentViewWriters, DatabaseDescriptor::setConcurrentViewWriters, Stage::multiThreadedLowSignalStage), - ACCORD_MIGRATION (false, "AccordMigrationStage", "request", DatabaseDescriptor::getAccordConcurrentOps, DatabaseDescriptor::setConcurrentAccordOps, Stage::multiThreadedLowSignalStage), + TRACKED_CAS (true, "TrackedCasOpsStage", "request", DatabaseDescriptor::getConcurrentTrackedCasOps, DatabaseDescriptor::setConcurrentTrackedCasOps, Stage::multiThreadedLowSignalStage), + ACCORD_MIGRATION (false, "AccordMigrationStage", "request", DatabaseDescriptor::getAccordConcurrentOps, DatabaseDescriptor::setConcurrentAccordOps, Stage::multiThreadedLowSignalStage), GOSSIP (true, "GossipStage", "internal", () -> 1, null, Stage::singleThreadedStage), REQUEST_RESPONSE (false, "RequestResponseStage", "request", FBUtilities::getAvailableProcessors, null, Stage::multiThreadedLowSignalStage), ANTI_ENTROPY (false, "AntiEntropyStage", "internal", () -> 1, null, Stage::singleThreadedStage), diff --git a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java index 9df5e2f201..72b8f0e1fb 100644 --- a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java +++ b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java @@ -213,6 +213,7 @@ public enum CassandraRelevantProperties DIAGNOSTIC_SNAPSHOT_INTERVAL_NANOS("cassandra.diagnostic_snapshot_interval_nanos", "60000000000"), DISABLE_AUTH_CACHES_REMOTE_CONFIGURATION("cassandra.disable_auth_caches_remote_configuration"), /** properties to disable certain behaviours for testing */ + DISABLE_CONSENSUS_REQUEST_FORWARDING("cassandra.disable_consensus_request_forwarding"), DISABLE_GOSSIP_ENDPOINT_REMOVAL("cassandra.gossip.disable_endpoint_removal"), DISABLE_PAXOS_AUTO_REPAIRS("cassandra.disable_paxos_auto_repairs"), DISABLE_PAXOS_STATE_FLUSH("cassandra.disable_paxos_state_flush"), diff --git a/src/java/org/apache/cassandra/config/Config.java b/src/java/org/apache/cassandra/config/Config.java index 2269cb2eda..bc24ece29b 100644 --- a/src/java/org/apache/cassandra/config/Config.java +++ b/src/java/org/apache/cassandra/config/Config.java @@ -208,6 +208,7 @@ public class Config public int concurrent_accord_operations = 32; public int concurrent_counter_writes = 32; public int concurrent_materialized_view_writes = 32; + public int concurrent_tracked_cas_ops = 32; public OptionaldPositiveInt available_processors = new OptionaldPositiveInt(CASSANDRA_AVAILABLE_PROCESSORS.getInt(OptionaldPositiveInt.UNDEFINED_VALUE)); public int memtable_flush_writers = 0; diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java index d7c997ba1e..78b61f7ac0 100644 --- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java +++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java @@ -2880,6 +2880,18 @@ public class DatabaseDescriptor conf.concurrent_accord_operations = concurrent_operations; } + public static int getConcurrentTrackedCasOps() + { + return conf.concurrent_tracked_cas_ops; + } + + public static void setConcurrentTrackedCasOps(int concurrent_tracked_cas_ops) + { + if (concurrent_tracked_cas_ops < 0) + throw new IllegalArgumentException("Concurrent tracked CAS ops must be non-negative"); + conf.concurrent_tracked_cas_ops = concurrent_tracked_cas_ops; + } + public static int getFlushWriters() { return conf.memtable_flush_writers; diff --git a/src/java/org/apache/cassandra/cql3/constraints/ConstraintViolationException.java b/src/java/org/apache/cassandra/cql3/constraints/ConstraintViolationException.java index ea7742ac18..a73b4475f0 100644 --- a/src/java/org/apache/cassandra/cql3/constraints/ConstraintViolationException.java +++ b/src/java/org/apache/cassandra/cql3/constraints/ConstraintViolationException.java @@ -18,6 +18,7 @@ package org.apache.cassandra.cql3.constraints; +import org.apache.cassandra.exceptions.CassandraExceptionCode; import org.apache.cassandra.exceptions.InvalidRequestException; /** @@ -29,4 +30,10 @@ public class ConstraintViolationException extends InvalidRequestException { super(msg); } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.CONSTRAINT_VIOLATION; + } } diff --git a/src/java/org/apache/cassandra/cql3/constraints/InvalidConstraintDefinitionException.java b/src/java/org/apache/cassandra/cql3/constraints/InvalidConstraintDefinitionException.java index 7ef32f0ca1..eac108413b 100644 --- a/src/java/org/apache/cassandra/cql3/constraints/InvalidConstraintDefinitionException.java +++ b/src/java/org/apache/cassandra/cql3/constraints/InvalidConstraintDefinitionException.java @@ -18,6 +18,7 @@ package org.apache.cassandra.cql3.constraints; +import org.apache.cassandra.exceptions.CassandraExceptionCode; import org.apache.cassandra.exceptions.InvalidRequestException; /** @@ -29,4 +30,10 @@ public class InvalidConstraintDefinitionException extends InvalidRequestExceptio { super(msg); } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.INVALID_CONSTRAINT_DEFINITION; + } } diff --git a/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java b/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java index 6fd03204ce..c1cc9e51dd 100644 --- a/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java @@ -58,7 +58,6 @@ 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; import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.db.partitions.PartitionUpdate; @@ -605,7 +604,7 @@ public class BatchStatement implements CQLStatement.CompositeCQLStatement if (key == null) { key = statement.metadata().partitioner.decorateKey(pks.get(0)); - casRequest = new CQL3CasRequest(statement.metadata(), key, conditionColumns, updatesRegularRows, updatesStaticRow, requestTime); + casRequest = new CQL3CasRequest(statement.metadata(), key, conditionColumns, updatesRegularRows, updatesStaticRow); } else if (!key.getKey().equals(pks.get(0))) { @@ -626,11 +625,7 @@ public class BatchStatement implements CQLStatement.CompositeCQLStatement if (slices.isEmpty()) continue; - for (Slice slice : slices) - { - casRequest.addRangeDeletion(slice, statement, statementOptions, timestamp, nowInSeconds); - } - + casRequest.addWriteFragment(statement, statementOptions, state.getClientState(), nowInSeconds); } else { @@ -644,7 +639,8 @@ public class BatchStatement implements CQLStatement.CompositeCQLStatement else if (columnsWithConditions != null) Iterables.addAll(columnsWithConditions, statement.getColumnsWithConditions()); } - casRequest.addRowUpdate(clustering, statement, statementOptions, timestamp, nowInSeconds); + + casRequest.addWriteFragment(statement, statementOptions, state.getClientState(), nowInSeconds); } } diff --git a/src/java/org/apache/cassandra/cql3/statements/BatchUpdatesCollector.java b/src/java/org/apache/cassandra/cql3/statements/BatchUpdatesCollector.java index 1db782f1c5..d7df3d26a0 100644 --- a/src/java/org/apache/cassandra/cql3/statements/BatchUpdatesCollector.java +++ b/src/java/org/apache/cassandra/cql3/statements/BatchUpdatesCollector.java @@ -133,13 +133,16 @@ final class BatchUpdatesCollector implements UpdatesCollector /** * Returns a collection containing all the mutations. - * + * * @param state state related to the client connection - * + * @param potentialTxnConflicts whether to allow potential transaction conflicts + * @param skipIndexValidation if true, skip index validation (used for CAS/transaction paths + * where validation happens later on the final materialized values) + * * @return a collection containing all the mutations. */ @Override - public List toMutations(ClientState state, PotentialTxnConflicts potentialTxnConflicts) + public List toMutations(ClientState state, PotentialTxnConflicts potentialTxnConflicts, boolean skipIndexValidation) { List ms = new ArrayList<>(); for (Map ksMap : mutationBuilders.values()) @@ -147,7 +150,8 @@ final class BatchUpdatesCollector implements UpdatesCollector for (IMutationBuilder builder : ksMap.values()) { IMutation mutation = builder.build(potentialTxnConflicts); - mutation.validateIndexedColumns(state); + if (!skipIndexValidation) + mutation.validateIndexedColumns(state); mutation.validateSize(MessagingService.current_version, CommitLogSegment.ENTRY_OVERHEAD_SIZE); ms.add(mutation); } diff --git a/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java b/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java index c2514d04ce..fc9bed7d7e 100644 --- a/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java +++ b/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java @@ -17,26 +17,24 @@ */ package org.apache.cassandra.cql3.statements; +import java.io.IOException; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.Objects; import java.util.TreeMap; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import accord.api.Update; import accord.primitives.Keys; import accord.primitives.Txn; import org.apache.cassandra.cql3.QueryOptions; -import org.apache.cassandra.cql3.UpdateParameters; import org.apache.cassandra.cql3.conditions.ColumnCondition; import org.apache.cassandra.db.Clustering; import org.apache.cassandra.db.Columns; @@ -45,28 +43,31 @@ import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.RegularAndStaticColumns; import org.apache.cassandra.db.SinglePartitionReadCommand; -import org.apache.cassandra.db.Slice; import org.apache.cassandra.db.Slices; +import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.filter.ClusteringIndexNamesFilter; import org.apache.cassandra.db.filter.ClusteringIndexSliceFilter; 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.marshal.TimeUUIDType; import org.apache.cassandra.db.partitions.FilteredPartition; -import org.apache.cassandra.db.partitions.Partition; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.index.IndexRegistry; +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableParams; -import org.apache.cassandra.service.CASRequest; +import org.apache.cassandra.schema.Schema; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.PreserveTimestamp; import org.apache.cassandra.service.accord.api.PartitionKey; import org.apache.cassandra.service.accord.serializers.TableMetadatas; import org.apache.cassandra.service.accord.serializers.TableMetadatasAndKeys; +import org.apache.cassandra.service.accord.serializers.Version; import org.apache.cassandra.service.accord.txn.TxnCondition; import org.apache.cassandra.service.accord.txn.TxnData; import org.apache.cassandra.service.accord.txn.TxnDataKeyValue; @@ -80,7 +81,6 @@ import org.apache.cassandra.service.accord.txn.TxnWrite; import org.apache.cassandra.service.paxos.Ballot; import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Dispatcher; -import org.apache.cassandra.utils.TimeUUID; import static com.google.common.base.Preconditions.checkState; import static org.apache.cassandra.service.StorageProxy.ConsensusAttemptResult; @@ -94,17 +94,13 @@ import static org.apache.cassandra.service.consensus.migration.ConsensusRequestR /** * Processed CAS conditions and update on potentially multiple rows of the same partition. */ -public class CQL3CasRequest implements CASRequest +public class CQL3CasRequest { - @SuppressWarnings("unused") - private static final Logger logger = LoggerFactory.getLogger(CQL3CasRequest.class); - public final TableMetadata metadata; public final DecoratedKey key; private final RegularAndStaticColumns conditionColumns; private final boolean updatesRegularRows; private final boolean updatesStaticRow; - private final Dispatcher.RequestTime requestTime; private boolean hasExists; // whether we have an exist or if not exist condition // Conditions on the static row. We keep it separate from 'conditions' as most things related to the static row are @@ -115,15 +111,13 @@ public class CQL3CasRequest implements CASRequest // 2) this allows to detect when contradictory conditions are set (not exists with some other conditions on the same row) private final TreeMap, RowCondition> conditions; - private final List updates = new ArrayList<>(); - private final List rangeDeletions = new ArrayList<>(); + private final List writeFragments = new ArrayList<>(); public CQL3CasRequest(TableMetadata metadata, DecoratedKey key, RegularAndStaticColumns conditionColumns, boolean updatesRegularRows, - boolean updatesStaticRow, - Dispatcher.RequestTime requestTime) + boolean updatesStaticRow) { this.metadata = metadata; this.key = key; @@ -131,23 +125,21 @@ public class CQL3CasRequest implements CASRequest this.conditionColumns = conditionColumns; this.updatesRegularRows = updatesRegularRows; this.updatesStaticRow = updatesStaticRow; - this.requestTime = requestTime; } - @Override public Dispatcher.RequestTime requestTime() { - return requestTime; + return Dispatcher.RequestTime.forImmediateExecution(); } - void addRowUpdate(Clustering clustering, ModificationStatement stmt, QueryOptions options, long timestamp, long nowInSeconds) - { - updates.add(new RowUpdate(clustering, stmt, options, timestamp, nowInSeconds)); - } - void addRangeDeletion(Slice slice, ModificationStatement stmt, QueryOptions options, long timestamp, long nowInSeconds) + void addWriteFragment(ModificationStatement stmt, QueryOptions options, ClientState clientState, long nowInSeconds) { - rangeDeletions.add(new RangeDeletion(slice, stmt, options, timestamp, nowInSeconds)); + // Create TxnWrite.Fragment directly using existing pattern + PartitionKey partitionKey = new PartitionKey(metadata.id, key); + List fragments = stmt.forTxn().getTxnWriteFragment( + writeFragments.size(), clientState, options, partitionKey, nowInSeconds); + writeFragments.addAll(fragments); } public void addNotExist(Clustering clustering) throws InvalidRequestException @@ -241,6 +233,9 @@ public class CQL3CasRequest implements CASRequest return new RegularAndStaticColumns(statics, regulars); } + /** + * The command to use to fetch the value to compare for the CAS. + */ public SinglePartitionReadCommand readCommand(long nowInSec) { assert staticConditions != null || !conditions.isEmpty(); @@ -289,111 +284,43 @@ public class CQL3CasRequest implements CASRequest private RegularAndStaticColumns updatedColumns() { RegularAndStaticColumns.Builder builder = RegularAndStaticColumns.builder(); - for (RowUpdate upd : updates) - builder.addAll(upd.stmt.updatedColumns()); + for (TxnWrite.Fragment fragment : writeFragments) + { + builder.addAll(fragment.baseUpdate.columns()); + if (!fragment.referenceOps.isEmpty()) + { + // Add columns from reference operations + fragment.referenceOps.getStatics().forEach(op -> builder.add(op.receiver())); + fragment.referenceOps.getRegulars().forEach(op -> builder.add(op.receiver())); + } + } return builder.build(); } + /** + * The updates to perform of a CAS success. The values fetched using the readFilter() + * are passed as argument. + */ public PartitionUpdate makeUpdates(FilteredPartition current, ClientState clientState, Ballot ballot) throws InvalidRequestException { - PartitionUpdate.Builder updateBuilder = new PartitionUpdate.Builder(metadata, key, updatedColumns(), conditions.size()); - long timeUuidNanos = 0; - for (RowUpdate upd : updates) - timeUuidNanos = upd.applyUpdates(current, updateBuilder, clientState, ballot.msb(), timeUuidNanos); - for (RangeDeletion upd : rangeDeletions) - upd.applyUpdates(current, updateBuilder, clientState); + if (writeFragments.isEmpty()) + return PartitionUpdate.emptyUpdate(metadata, key); + + PartitionUpdate.Builder updateBuilder = new PartitionUpdate.Builder( + metadata, key, updatedColumns(), writeFragments.size()); + + // Create TxnData from read results + TxnDataKeyValue txnDataValue = new TxnDataKeyValue(current.rowIterator(false)); + TxnData txnData = TxnData.of(txnDataName(CAS_READ), txnDataValue); + + for (TxnWrite.Fragment fragment : writeFragments) + fragment.completeToBuilder(updateBuilder, txnData, ballot, current, clientState); PartitionUpdate partitionUpdate = updateBuilder.build(); IndexRegistry.obtain(metadata).validate(partitionUpdate, clientState); - return partitionUpdate; } - private static class CASUpdateParameters extends UpdateParameters - { - final long timeUuidMsb; - long timeUuidNanos; - - public CASUpdateParameters(TableMetadata metadata, ClientState state, QueryOptions options, long timestamp, long nowInSec, int ttl, Map prefetchedRows, long timeUuidMsb, long timeUuidNanos) throws InvalidRequestException - { - super(metadata, state, options, timestamp, nowInSec, ttl, prefetchedRows); - this.timeUuidMsb = timeUuidMsb; - this.timeUuidNanos = timeUuidNanos; - } - - public byte[] nextTimeUUIDAsBytes() - { - return TimeUUID.toBytes(timeUuidMsb, TimeUUIDType.signedBytesToNativeLong(timeUuidNanos++)); - } - } - - /** - * Due to some operation on lists, we can't generate the update that a given Modification statement does before - * we get the values read by the initial read of Paxos. A RowUpdate thus just store the relevant information - * (include the statement iself) to generate those updates. We'll have multiple RowUpdate for a Batch, otherwise - * we'll have only one. - */ - private class RowUpdate - { - private final Clustering clustering; - private final ModificationStatement stmt; - private final QueryOptions options; - private final long timestamp; - private final long nowInSeconds; - - private RowUpdate(Clustering clustering, ModificationStatement stmt, QueryOptions options, long timestamp, long nowInSeconds) - { - this.clustering = clustering; - this.stmt = stmt; - this.options = options; - this.timestamp = timestamp; - this.nowInSeconds = nowInSeconds; - } - - long applyUpdates(FilteredPartition current, PartitionUpdate.Builder updateBuilder, ClientState state, long timeUuidMsb, long timeUuidNanos) - { - Map map = stmt.requiresRead() ? Collections.singletonMap(key, current) : null; - CASUpdateParameters params = - new CASUpdateParameters(metadata, state, options, timestamp, nowInSeconds, - stmt.getTimeToLive(options), map, timeUuidMsb, timeUuidNanos); - stmt.addUpdateForKey(updateBuilder, clustering, params); - return params.timeUuidNanos; - } - } - - private class RangeDeletion - { - private final Slice slice; - private final ModificationStatement stmt; - private final QueryOptions options; - private final long timestamp; - private final long nowInSeconds; - - private RangeDeletion(Slice slice, ModificationStatement stmt, QueryOptions options, long timestamp, long nowInSeconds) - { - this.slice = slice; - this.stmt = stmt; - this.options = options; - this.timestamp = timestamp; - this.nowInSeconds = nowInSeconds; - } - - void applyUpdates(FilteredPartition current, PartitionUpdate.Builder updateBuilder, ClientState state) - { - // No slice statements currently require a read, but this maintains consistency with RowUpdate, and future proofs us - Map map = stmt.requiresRead() ? Collections.singletonMap(key, current) : null; - UpdateParameters params = - new UpdateParameters(metadata, - state, - options, - timestamp, - nowInSeconds, - stmt.getTimeToLive(options), - map); - stmt.addUpdateForKey(updateBuilder, slice, params); - } - } - private static abstract class RowCondition { public final Clustering clustering; @@ -436,6 +363,21 @@ public class CQL3CasRequest implements CASRequest TxnReference txnReference = TxnReference.row(txnDataName(CAS_READ)); return new TxnCondition.Exists(txnReference, TxnCondition.Kind.IS_NULL); } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NotExistCondition that = (NotExistCondition) o; + return Objects.equals(clustering, that.clustering); + } + + @Override + public int hashCode() + { + return Objects.hash(clustering); + } } private static class ExistCondition extends RowCondition implements ToCQL @@ -461,6 +403,21 @@ public class CQL3CasRequest implements CASRequest TxnReference txnReference = TxnReference.row(txnDataName(CAS_READ)); return new TxnCondition.Exists(txnReference, TxnCondition.Kind.IS_NOT_NULL); } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExistCondition that = (ExistCondition) o; + return Objects.equals(clustering, that.clustering); + } + + @Override + public int hashCode() + { + return Objects.hash(clustering); + } } private static class ColumnsConditions extends RowCondition @@ -496,6 +453,22 @@ public class CQL3CasRequest implements CASRequest { return new TxnCondition.ColumnConditionsAdapter(clustering, conditions); } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ColumnsConditions that = (ColumnsConditions) o; + return Objects.equals(clustering, that.clustering) && + Objects.equals(conditions, that.conditions); + } + + @Override + public int hashCode() + { + return Objects.hash(clustering, conditions); + } } @Override @@ -505,6 +478,30 @@ public class CQL3CasRequest implements CASRequest } @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CQL3CasRequest that = (CQL3CasRequest) o; + return updatesRegularRows == that.updatesRegularRows && + updatesStaticRow == that.updatesStaticRow && + hasExists == that.hasExists && + Objects.equals(metadata.id, that.metadata.id) && // Compare table IDs instead of full metadata + Objects.equals(key, that.key) && + Objects.equals(conditionColumns, that.conditionColumns) && + Objects.equals(staticConditions, that.staticConditions) && + Objects.equals(conditions, that.conditions) && + Objects.equals(writeFragments, that.writeFragments); + } + + @Override + public int hashCode() + { + return Objects.hash(metadata.id, key, conditionColumns, updatesRegularRows, updatesStaticRow, + hasExists, staticConditions, conditions, writeFragments); + } + public Txn toAccordTxn(ClusterMetadata cm, ConsistencyLevel consistencyLevel, ConsistencyLevel commitConsistencyLevel, ClientState clientState, long nowInSecs) { SinglePartitionReadCommand readCommand = readCommand(nowInSecs); @@ -531,7 +528,7 @@ public class CQL3CasRequest implements CASRequest TableParams tableParams = tableMetadata.params; commitConsistencyLevel = tableParams.transactionalMode.commitCLForMode(tableParams.transactionalMigrationFrom, commitConsistencyLevel, cm, tableMetadata.id, key.getToken()); // CAS requires using the new txn timestamp to correctly linearize some kinds of updates - return new TxnUpdate(tables, createWriteFragments(clientState), createCondition(), commitConsistencyLevel, PreserveTimestamp.no); + return new TxnUpdate(tables, writeFragments, createCondition(), commitConsistencyLevel, PreserveTimestamp.no); } private TxnCondition createCondition() @@ -548,30 +545,6 @@ public class CQL3CasRequest implements CASRequest return conditions.size() == 1 ? txnConditions.get(0) : new TxnCondition.BooleanGroup(TxnCondition.Kind.AND, txnConditions); } - private List createWriteFragments(ClientState state) - { - PartitionKey partitionKey = new PartitionKey(metadata.id, key); - List fragments = new ArrayList<>(); - int idx = 0; - for (RowUpdate update : updates) - { - // Some operations may need to migrate to run in the transaction, so need to call forTxn to make sure this - // happens. - // see CASSANDRA-18337 - ModificationStatement modification = update.stmt.forTxn(); - QueryOptions options = update.options; - fragments.addAll(modification.getTxnWriteFragment(idx++, state, options, partitionKey)); - } - for (RangeDeletion rangeDeletion : rangeDeletions) - { - ModificationStatement modification = rangeDeletion.stmt; - QueryOptions options = rangeDeletion.options; - fragments.addAll(modification.getTxnWriteFragment(idx++, state, options, partitionKey)); - } - return fragments; - } - - @Override public ConsensusAttemptResult toCasResult(TxnResult txnResult) { if (txnResult.kind() == retry_new_protocol) @@ -581,4 +554,197 @@ public class CQL3CasRequest implements CASRequest TxnDataKeyValue partition = (TxnDataKeyValue)txnData.get(txnDataName(CAS_READ)); return casResult(partition != null ? partition.rowIterator(false) : null); } + + public static final Serializer serializer = new Serializer(); + /** + * IVersionedSerializer for CQL3CasRequest to enable CAS forwarding between coordinators. + * + */ + public static class Serializer implements IVersionedSerializer + { + private static final int UPDATES_REGULAR_ROWS = 0x01; + private static final int UPDATES_STATIC_ROW = 0x02; + private static final int HAS_EXISTS = 0x04; + + private static final byte CONDITION_NULL = 0; + private static final byte CONDITION_NOT_EXIST = 1; + private static final byte CONDITION_EXIST = 2; + private static final byte CONDITION_COLUMNS = 3; + + @Override + public void serialize(CQL3CasRequest request, DataOutputPlus out, int version) throws IOException + { + int flags = (request.updatesRegularRows ? UPDATES_REGULAR_ROWS : 0) + | (request.updatesStaticRow ? UPDATES_STATIC_ROW : 0) + | (request.hasExists ? HAS_EXISTS : 0) + ; + out.write(flags); + + request.metadata.id.serializeCompact(out); + DecoratedKey.serializer.serialize(request.key, out, version); + + Columns.serializer.serialize(request.conditionColumns.statics, out); + Columns.serializer.serialize(request.conditionColumns.regulars, out); + + serializeRowCondition(request.staticConditions, out, version); + + out.writeUnsignedVInt32(request.conditions.size()); + for (Map.Entry, RowCondition> entry : request.conditions.entrySet()) + { + Clustering.serializer.serialize(entry.getKey(), out, version, request.metadata.comparator.subtypes()); + serializeRowCondition(entry.getValue(), out, version); + } + + out.writeUnsignedVInt32(request.writeFragments.size()); + TableMetadatas tableMetadatas = TableMetadatas.of(request.metadata); + for (TxnWrite.Fragment fragment : request.writeFragments) + TxnWrite.Fragment.serializer.serialize(fragment, tableMetadatas, out, Version.findBestMatchForMessagingVersion(version)); + } + + @Override + public CQL3CasRequest deserialize(DataInputPlus in, int version) throws IOException + { + int flags = in.readUnsignedByte(); + boolean updatesRegularRows = (flags & UPDATES_REGULAR_ROWS) != 0; + boolean updatesStaticRow = (flags & UPDATES_STATIC_ROW) != 0; + boolean hasExists = (flags & HAS_EXISTS) != 0; + + TableId tableId = TableId.deserializeCompact(in); + TableMetadata metadata = Schema.instance.getTableMetadata(tableId); + if (metadata == null) + throw new IOException("Unknown table ID in CQL3CasRequest deserialization: " + tableId); + + DecoratedKey key = (DecoratedKey) DecoratedKey.serializer.deserialize(in, version); + + Columns statics = Columns.serializer.deserialize(in, metadata); + Columns regulars = Columns.serializer.deserialize(in, metadata); + RegularAndStaticColumns conditionColumns = new RegularAndStaticColumns(statics, regulars); + + CQL3CasRequest request = new CQL3CasRequest(metadata, key, conditionColumns, updatesRegularRows, updatesStaticRow); + request.hasExists = hasExists; + + request.staticConditions = deserializeRowCondition(in, version, metadata, Clustering.STATIC_CLUSTERING); + + int conditionsCount = in.readUnsignedVInt32(); + for (int i = 0; i < conditionsCount; i++) + { + Clustering clustering = Clustering.serializer.deserialize(in, version, metadata.comparator.subtypes()); + RowCondition condition = deserializeRowCondition(in, version, metadata, clustering); + request.conditions.put(clustering, condition); + } + + int fragmentCount = in.readUnsignedVInt32(); + TableMetadatas tableMetadatas = TableMetadatas.of(metadata); + for (int i = 0; i < fragmentCount; i++) + { + PartitionKey partitionKey = new PartitionKey(metadata.id, request.key); + TxnWrite.Fragment fragment = TxnWrite.Fragment.serializer.deserialize(partitionKey, tableMetadatas, in, Version.findBestMatchForMessagingVersion(version)); + request.writeFragments.add(fragment); + } + + return request; + } + + @Override + public long serializedSize(CQL3CasRequest request, int version) + { + // Flags byte + long size = 1; + + size += request.metadata.id.serializedCompactSize(); + size += DecoratedKey.serializer.serializedSize(request.key, version); + size += Columns.serializer.serializedSize(request.conditionColumns.statics); + size += Columns.serializer.serializedSize(request.conditionColumns.regulars); + size += rowConditionSize(request.staticConditions, version); + + size += TypeSizes.sizeofUnsignedVInt(request.conditions.size()); + for (Map.Entry, RowCondition> entry : request.conditions.entrySet()) + { + size += Clustering.serializer.serializedSize(entry.getKey(), version, request.metadata.comparator.subtypes()); + size += rowConditionSize(entry.getValue(), version); + } + + size += TypeSizes.sizeofUnsignedVInt(request.writeFragments.size()); + for (TxnWrite.Fragment fragment : request.writeFragments) + size += TxnWrite.Fragment.serializer.serializedSize(fragment, TableMetadatas.of(request.metadata), Version.findBestMatchForMessagingVersion(version)); + + return size; + } + + private void serializeRowCondition(RowCondition condition, DataOutputPlus out, int version) throws IOException + { + if (condition == null) + { + out.writeByte(CONDITION_NULL); + } + else if (condition instanceof NotExistCondition) + { + out.writeByte(CONDITION_NOT_EXIST); + // Don't serialize clustering here - it's already serialized in the conditions map + } + else if (condition instanceof ExistCondition) + { + out.writeByte(CONDITION_EXIST); + // Don't serialize clustering here - it's already serialized in the conditions map + } + else if (condition instanceof ColumnsConditions) + { + out.writeByte(CONDITION_COLUMNS); + ColumnsConditions cc = (ColumnsConditions) condition; + out.writeUnsignedVInt32(cc.conditions.size()); + + // Serialize each ColumnCondition.Bound using adapted pattern + for (ColumnCondition.Bound bound : cc.conditions) + ColumnCondition.Bound.serializer.serialize(bound, TableMetadatas.of(bound.table), out); + } + else + { + throw new IOException("Unknown RowCondition type: " + condition.getClass()); + } + } + + private RowCondition deserializeRowCondition(DataInputPlus in, int version, TableMetadata metadata, Clustering clustering) throws IOException + { + byte type = in.readByte(); + switch (type) + { + case CONDITION_NULL: + return null; + case CONDITION_NOT_EXIST: + return new NotExistCondition(clustering); + case CONDITION_EXIST: + return new ExistCondition(clustering); + case CONDITION_COLUMNS: + int conditionsCount = in.readUnsignedVInt32(); + ColumnsConditions columnsConditions = new ColumnsConditions(clustering); + + // Deserialize each ColumnCondition.Bound + for (int i = 0; i < conditionsCount; i++) + { + ColumnCondition.Bound bound = ColumnCondition.Bound.serializer.deserialize(TableMetadatas.of(metadata), in); + columnsConditions.conditions.add(bound); + } + + return columnsConditions; + default: + throw new IOException("Unknown RowCondition type: " + type); + } + } + + private long rowConditionSize(RowCondition condition, int version) + { + long size = 1; // type byte + + if (condition instanceof ColumnsConditions) + { + ColumnsConditions cc = (ColumnsConditions) condition; + size += TypeSizes.sizeofUnsignedVInt(cc.conditions.size()); + // Calculate size for each ColumnCondition.Bound + for (ColumnCondition.Bound bound : cc.conditions) + size += ColumnCondition.Bound.serializer.serializedSize(bound, TableMetadatas.of(bound.table)); + } + + return size; + } + } } diff --git a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java index 4a580fb621..ba152c9be2 100644 --- a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java @@ -729,11 +729,11 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa type.isUpdate()? "updates" : "deletions"); Clustering clustering = Iterables.getOnlyElement(createClustering(options, clientState)); - CQL3CasRequest request = new CQL3CasRequest(metadata(), key, conditionColumns(), updatesRegularRows(), updatesStaticRow(), requestTime); + CQL3CasRequest request = new CQL3CasRequest(metadata(), key, conditionColumns(), updatesRegularRows(), updatesStaticRow()); addConditions(clustering, request, options); - request.addRowUpdate(clustering, this, options, timestamp, nowInSeconds); + request.addWriteFragment(this, options, clientState, nowInSeconds); return request; } @@ -906,6 +906,47 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa return null; } + /** + * Convert statement into a list of mutations to apply on the server + * + * @param state the client state + * @param options value for prepared statement markers + * @param local if true, any requests (for collections) performed by getMutation should be done locally only. + * @param timestamp the current timestamp in microseconds to use if no timestamp is user provided. + * @param nowInSeconds the current time in seconds + * @param requestTime the request time + * @param skipIndexValidation if true, skip index validation (used for CAS/transaction paths + * where validation happens later on the final materialized values) + * + * @return list of the mutations + */ + public List getMutations(ClientState state, + QueryOptions options, + boolean local, + long timestamp, + long nowInSeconds, + Dispatcher.RequestTime requestTime, + boolean skipIndexValidation) + { + List keys = buildPartitionKeyNames(options, state); + + if (keys.size() == 1) + { + SingleTableSinglePartitionUpdatesCollector collector = new SingleTableSinglePartitionUpdatesCollector(metadata, updatedColumns); + addUpdates(collector, keys, state, options, local, timestamp, nowInSeconds, requestTime); + // local means this is test or internal things that are bypassing distributed system modification/checks + return collector.toMutations(state, local ? PotentialTxnConflicts.ALLOW : PotentialTxnConflicts.DISALLOW, skipIndexValidation); + } + else + { + HashMultiset perPartitionKeyCounts = HashMultiset.create(keys); + SingleTableUpdatesCollector collector = new SingleTableUpdatesCollector(metadata, updatedColumns, perPartitionKeyCounts); + addUpdates(collector, keys, state, options, local, timestamp, nowInSeconds, requestTime); + // local means this is test or internal things that are bypassing distributed system modification/checks + return collector.toMutations(state, local ? PotentialTxnConflicts.ALLOW : PotentialTxnConflicts.DISALLOW, skipIndexValidation); + } + } + /** * Convert statement into a list of mutations to apply on the server * @@ -923,28 +964,15 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa long nowInSeconds, Dispatcher.RequestTime requestTime) { - List keys = buildPartitionKeyNames(options, state); - - if (keys.size() == 1) - { - SingleTableSinglePartitionUpdatesCollector collector = new SingleTableSinglePartitionUpdatesCollector(metadata, updatedColumns); - addUpdates(collector, keys, state, options, local, timestamp, nowInSeconds, requestTime); - // local means this is test or internal things that are bypassing distributed system modification/checks - return collector.toMutations(state, local ? PotentialTxnConflicts.ALLOW : PotentialTxnConflicts.DISALLOW); - } - else - { - HashMultiset perPartitionKeyCounts = HashMultiset.create(keys); - SingleTableUpdatesCollector collector = new SingleTableUpdatesCollector(metadata, updatedColumns, perPartitionKeyCounts); - addUpdates(collector, keys, state, options, local, timestamp, nowInSeconds, requestTime); - // local means this is test or internal things that are bypassing distributed system modification/checks - return collector.toMutations(state, local ? PotentialTxnConflicts.ALLOW : PotentialTxnConflicts.DISALLOW); - } + return getMutations(state, options, local, timestamp, nowInSeconds, requestTime, false); } - public List getTxnUpdate(ClientState state, QueryOptions options) + public List getTxnUpdate(ClientState state, QueryOptions options, long nowInSeconds) { - List mutations = getMutations(state, options, false, 0, 0, new Dispatcher.RequestTime(0, 0)); + // Skip index validation here because validation happens later on the final materialized values + // in CQL3CasRequest.makeUpdates() + List mutations = getMutations(state, options, false, 0, nowInSeconds, new Dispatcher.RequestTime(0, 0), true); + // TODO: Temporary fix for CASSANDRA-20079 if (mutations.isEmpty()) return Collections.emptyList(); List updates = new ArrayList<>(mutations.size()); @@ -1004,22 +1032,22 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa return operations.allSubstitutions(); } - public List getTxnWriteFragment(int index, ClientState state, QueryOptions options, PartitionKey partitionKey) + public List getTxnWriteFragment(int index, ClientState state, QueryOptions options, PartitionKey partitionKey, long nowInSeconds) { return getTxnWriteFragment(index, state, options, baseUpdate -> { Invariants.require(baseUpdate.partitionKey().equals(partitionKey.partitionKey()), "PartitionUpdate generated a partition key different than the one expected"); return partitionKey; - }); + }, nowInSeconds); } - public List getTxnWriteFragment(int index, ClientState state, QueryOptions options, KeyCollector keyCollector) + public List getTxnWriteFragment(int index, ClientState state, QueryOptions options, KeyCollector keyCollector, long nowInSeconds) { - return getTxnWriteFragment(index, state, options, baseUpdate -> keyCollector.collect(baseUpdate.metadata(), baseUpdate.partitionKey())); + return getTxnWriteFragment(index, state, options, baseUpdate -> keyCollector.collect(baseUpdate.metadata(), baseUpdate.partitionKey()), nowInSeconds); } - private List getTxnWriteFragment(int index, ClientState state, QueryOptions options, java.util.function.Function keyCollector) + private List getTxnWriteFragment(int index, ClientState state, QueryOptions options, java.util.function.Function keyCollector, long nowInSeconds) { - List baseUpdates = getTxnUpdate(state, options); + List baseUpdates = getTxnUpdate(state, options, nowInSeconds); TxnReferenceOperations referenceOps = getTxnReferenceOps(options, state); long timestamp = attrs.isTimestampSet() ? attrs.getTimestamp(TxnWrite.NO_TIMESTAMP, options) : TxnWrite.NO_TIMESTAMP; if (baseUpdates.size() == 1) diff --git a/src/java/org/apache/cassandra/cql3/statements/SingleTableSinglePartitionUpdatesCollector.java b/src/java/org/apache/cassandra/cql3/statements/SingleTableSinglePartitionUpdatesCollector.java index e8bac387fe..d217090aca 100644 --- a/src/java/org/apache/cassandra/cql3/statements/SingleTableSinglePartitionUpdatesCollector.java +++ b/src/java/org/apache/cassandra/cql3/statements/SingleTableSinglePartitionUpdatesCollector.java @@ -80,16 +80,16 @@ final class SingleTableSinglePartitionUpdatesCollector implements UpdatesCollect * Returns a collection containing all the mutations. */ @Override - public List toMutations(ClientState state, PotentialTxnConflicts potentialTxnConflicts) + public List toMutations(ClientState state, PotentialTxnConflicts potentialTxnConflicts, boolean skipIndexValidation) { // 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, potentialTxnConflicts)); + return Collections.singletonList(createMutation(state, builder, potentialTxnConflicts, skipIndexValidation)); } - private IMutation createMutation(ClientState state, PartitionUpdate.Builder builder, PotentialTxnConflicts potentialTxnConflicts) + private IMutation createMutation(ClientState state, PartitionUpdate.Builder builder, PotentialTxnConflicts potentialTxnConflicts, boolean skipIndexValidation) { IMutation mutation; @@ -100,7 +100,8 @@ final class SingleTableSinglePartitionUpdatesCollector implements UpdatesCollect else mutation = new Mutation(MutationId.fixme(), builder.build(), potentialTxnConflicts); - mutation.validateIndexedColumns(state); + if (!skipIndexValidation) + mutation.validateIndexedColumns(state); mutation.validateSize(MessagingService.current_version, CommitLogSegment.ENTRY_OVERHEAD_SIZE); return mutation; } diff --git a/src/java/org/apache/cassandra/cql3/statements/SingleTableUpdatesCollector.java b/src/java/org/apache/cassandra/cql3/statements/SingleTableUpdatesCollector.java index ecd1b9fe69..3ea83b9792 100644 --- a/src/java/org/apache/cassandra/cql3/statements/SingleTableUpdatesCollector.java +++ b/src/java/org/apache/cassandra/cql3/statements/SingleTableUpdatesCollector.java @@ -97,24 +97,24 @@ final class SingleTableUpdatesCollector implements UpdatesCollector * @return a collection containing all the mutations. */ @Override - public List toMutations(ClientState state, PotentialTxnConflicts potentialTxnConflicts) + public List toMutations(ClientState state, PotentialTxnConflicts potentialTxnConflicts, boolean skipIndexValidation) { if (puBuilders.size() == 1) { PartitionUpdate.Builder builder = puBuilders.values().iterator().next(); - return Collections.singletonList(createMutation(state, builder, potentialTxnConflicts)); + return Collections.singletonList(createMutation(state, builder, potentialTxnConflicts, skipIndexValidation)); } List ms = new ArrayList<>(puBuilders.size()); for (PartitionUpdate.Builder builder : puBuilders.values()) { - IMutation mutation = createMutation(state, builder, potentialTxnConflicts); + IMutation mutation = createMutation(state, builder, potentialTxnConflicts, skipIndexValidation); ms.add(mutation); } return ms; } - private IMutation createMutation(ClientState state, PartitionUpdate.Builder builder, PotentialTxnConflicts potentialTxnConflicts) + private IMutation createMutation(ClientState state, PartitionUpdate.Builder builder, PotentialTxnConflicts potentialTxnConflicts, boolean skipIndexValidation) { IMutation mutation; @@ -125,7 +125,8 @@ final class SingleTableUpdatesCollector implements UpdatesCollector else mutation = new Mutation(MutationId.fixme(), builder.build(), potentialTxnConflicts); - mutation.validateIndexedColumns(state); + if (!skipIndexValidation) + mutation.validateIndexedColumns(state); mutation.validateSize(MessagingService.current_version, CommitLogSegment.ENTRY_OVERHEAD_SIZE); return mutation; } diff --git a/src/java/org/apache/cassandra/cql3/statements/TransactionStatement.java b/src/java/org/apache/cassandra/cql3/statements/TransactionStatement.java index 52039e7bef..377f24b5c3 100644 --- a/src/java/org/apache/cassandra/cql3/statements/TransactionStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/TransactionStatement.java @@ -355,14 +355,14 @@ public class TransactionStatement implements CQLStatement.CompositeCQLStatement, return new Keys(keySet); } - List createWriteFragments(ClientState state, QueryOptions options, Map autoReads, TableMetadatasAndKeys.KeyCollector keyCollector) + List createWriteFragments(ClientState state, QueryOptions options, Map autoReads, TableMetadatasAndKeys.KeyCollector keyCollector, long nowInSeconds) { List fragments = new ArrayList<>(updates.size()); int idx = 0; for (ModificationStatement modification : updates) { minEpoch = Math.max(minEpoch, modification.metadata().epoch.getEpoch()); - fragments.addAll(modification.getTxnWriteFragment(idx, state, options, keyCollector)); + fragments.addAll(modification.getTxnWriteFragment(idx, state, options, keyCollector, nowInSeconds)); if (modification.allReferenceOperations().stream().anyMatch(ReferenceOperation::requiresRead)) { @@ -477,7 +477,8 @@ public class TransactionStatement implements CQLStatement.CompositeCQLStatement, else { Int2ObjectHashMap autoReads = new Int2ObjectHashMap<>(); - List writeFragments = createWriteFragments(state, options, autoReads, keyCollector); + long nowInSeconds = options.getNowInSec(FBUtilities.nowInSeconds()); + List writeFragments = createWriteFragments(state, options, autoReads, keyCollector, nowInSeconds); List reads = createNamedReads(options, autoReads, keyCollector); if (writeFragments.isEmpty()) // ModificationStatement yield no Mutation (DELETE WHERE pk=0 AND c < 0 AND c > 0 -- matches no keys; so has no mutation) { diff --git a/src/java/org/apache/cassandra/cql3/statements/UpdatesCollector.java b/src/java/org/apache/cassandra/cql3/statements/UpdatesCollector.java index a3867b608f..e5b3d6bb2d 100644 --- a/src/java/org/apache/cassandra/cql3/statements/UpdatesCollector.java +++ b/src/java/org/apache/cassandra/cql3/statements/UpdatesCollector.java @@ -31,5 +31,20 @@ import org.apache.cassandra.service.ClientState; public interface UpdatesCollector { PartitionUpdate.Builder getPartitionUpdateBuilder(TableMetadata metadata, DecoratedKey dk, ConsistencyLevel consistency); - List toMutations(ClientState state, PotentialTxnConflicts allowPotentialTxnConflicts); + + /** + * Builds mutations from the collected updates. + * + * @param state the client state + * @param allowPotentialTxnConflicts whether to allow potential transaction conflicts + * @param skipIndexValidation if true, skip index validation (used for CAS/transaction paths + * where validation happens later on the final materialized values) + * @return the list of mutations + */ + List toMutations(ClientState state, PotentialTxnConflicts allowPotentialTxnConflicts, boolean skipIndexValidation); + + default List toMutations(ClientState state, PotentialTxnConflicts allowPotentialTxnConflicts) + { + return toMutations(state, allowPotentialTxnConflicts, false); + } } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java index d3e08dfee5..fc1c2343f1 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java @@ -174,9 +174,6 @@ public final class CreateIndexStatement extends AlterSchemaStatement if (table.isView()) throw ire(MATERIALIZED_VIEWS_NOT_SUPPORTED); - if (keyspace.replicationStrategy.hasTransientReplicas()) - throw new InvalidRequestException(TRANSIENTLY_REPLICATED_KEYSPACE_NOT_SUPPORTED); - // guardrails to limit number of secondary indexes per table. Guardrails.secondaryIndexesPerTable.guard(table.indexes.size() + 1, Strings.isNullOrEmpty(indexName) diff --git a/src/java/org/apache/cassandra/db/EmbeddableSinglePartitionReadCommand.java b/src/java/org/apache/cassandra/db/EmbeddableSinglePartitionReadCommand.java new file mode 100644 index 0000000000..623af24333 --- /dev/null +++ b/src/java/org/apache/cassandra/db/EmbeddableSinglePartitionReadCommand.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db; + +import java.io.IOException; + +import com.google.common.base.Preconditions; + +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.reads.tracked.TrackedRead.DataRequest; +import org.apache.cassandra.service.reads.tracked.TrackedRead.SummaryRequest; + +import static org.apache.cassandra.db.ReadKind.UNTRACKED; + +/** + * Interface for read command that allows it be serialized and embedded in another message. Used in Paxos + * to provide a common base class to serialize for tracked and untracked reads. Tracked reads contain + * additional information needed to execute the read beyond the read command itself so an additional interface + * is needed. + */ +public interface EmbeddableSinglePartitionReadCommand +{ + ReadKind kind(); + + default boolean isTracked() + { + return kind().isTracked(); + } + + TableMetadata metadata(); + + DecoratedKey partitionKey(); + + IVersionedSerializer serializer = new IVersionedSerializer<>() + { + @Override + public void serialize(EmbeddableSinglePartitionReadCommand command, DataOutputPlus out, int version) throws IOException + { + if (version >= MessagingService.VERSION_61) + ReadKind.serializer.serialize(command.kind(), out); + else + Preconditions.checkArgument(command.kind() == UNTRACKED); + + switch (command.kind()) + { + case UNTRACKED: + ReadCommand.serializer.serialize((ReadCommand) command, out, version); + break; + case TRACKED_DATA: + DataRequest.serializer.serialize((DataRequest) command, out, version); + break; + case TRACKED_SUMMARY: + SummaryRequest.serializer.serialize((SummaryRequest) command, out, version); + break; + default: + throw new IllegalStateException("Unhandled kind: " + command.kind()); + } + } + + @Override + public EmbeddableSinglePartitionReadCommand deserialize(DataInputPlus in, int version) throws IOException + { + + ReadKind kind = version >= MessagingService.VERSION_61 ? ReadKind.serializer.deserialize(in) : UNTRACKED; + switch (kind) + { + case UNTRACKED: + return (SinglePartitionReadCommand)ReadCommand.serializer.deserialize(in, version); + case TRACKED_DATA: + return DataRequest.serializer.deserialize(in, version); + case TRACKED_SUMMARY: + return SummaryRequest.serializer.deserialize(in, version); + default: + throw new IllegalStateException("Unhandled kind: " + kind); + } + } + + @Override + public long serializedSize(EmbeddableSinglePartitionReadCommand command, int version) + { + long size = 0; + if (version >= MessagingService.VERSION_61) + size += ReadKind.serializer.serializedSize(command.kind()); + else + Preconditions.checkArgument(command.kind() == UNTRACKED); + + switch (command.kind()) + { + case UNTRACKED: + return size + ReadCommand.serializer.serializedSize((ReadCommand) command, version); + case TRACKED_DATA: + return size + DataRequest.serializer.serializedSize((DataRequest) command, version); + case TRACKED_SUMMARY: + return size + SummaryRequest.serializer.serializedSize((SummaryRequest) command, version); + default: + throw new IllegalStateException("Unhandled kind: " + command.kind()); + } + } + }; +} diff --git a/src/java/org/apache/cassandra/db/IReadResponse.java b/src/java/org/apache/cassandra/db/IReadResponse.java new file mode 100644 index 0000000000..0aa161799e --- /dev/null +++ b/src/java/org/apache/cassandra/db/IReadResponse.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.db; + +import java.io.IOException; + +import com.google.common.base.Preconditions; + +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.service.reads.tracked.TrackedDataResponse; +import org.apache.cassandra.service.reads.tracked.TrackedSummaryResponse; + +import static org.apache.cassandra.db.ReadKind.UNTRACKED; + +public interface IReadResponse +{ + ReadKind kind(); + + IVersionedSerializer serializer = new IVersionedSerializer<>() + { + @Override + public void serialize(IReadResponse response, DataOutputPlus out, int version) throws IOException + { + if (version >= MessagingService.VERSION_61) + ReadKind.serializer.serialize(response.kind(), out); + else + Preconditions.checkArgument(response.kind() == UNTRACKED); + + switch (response.kind()) + { + case UNTRACKED: + ReadResponse.serializer.serialize((ReadResponse) response, out, version); + break; + case TRACKED_DATA: + TrackedDataResponse.serializer.serialize((TrackedDataResponse) response, out, version); + break; + case TRACKED_SUMMARY: + TrackedSummaryResponse.serializer.serialize((TrackedSummaryResponse) response, out, version); + break; + default: + throw new IllegalStateException("Unhandled kind: " + response.kind()); + } + } + + @Override + public IReadResponse deserialize(DataInputPlus in, int version) throws IOException + { + + ReadKind kind = version >= MessagingService.VERSION_61 ? ReadKind.serializer.deserialize(in) : UNTRACKED; + switch (kind) + { + case UNTRACKED: + return ReadResponse.serializer.deserialize(in, version); + case TRACKED_DATA: + return TrackedDataResponse.serializer.deserialize(in, version); + case TRACKED_SUMMARY: + return TrackedSummaryResponse.serializer.deserialize(in, version); + default: + throw new IllegalStateException("Unhandled kind: " + kind); + } + } + + @Override + public long serializedSize(IReadResponse response, int version) + { + long size = 0; + + if (version >= MessagingService.VERSION_61) + size += ReadKind.serializer.serializedSize(response.kind()); + else + Preconditions.checkArgument(response.kind() == UNTRACKED); + + switch (response.kind()) + { + case UNTRACKED: + return size + ReadResponse.serializer.serializedSize((ReadResponse) response, version); + case TRACKED_DATA: + return size + TrackedDataResponse.serializer.serializedSize((TrackedDataResponse) response, version); + case TRACKED_SUMMARY: + return size + TrackedSummaryResponse.serializer.serializedSize((TrackedSummaryResponse) response, version); + default: + throw new IllegalStateException("Unhandled kind: " + response.kind()); + } + } + }; +} diff --git a/src/java/org/apache/cassandra/db/KeyspaceNotDefinedException.java b/src/java/org/apache/cassandra/db/KeyspaceNotDefinedException.java index 9d07f5e9b7..a99536c7f9 100644 --- a/src/java/org/apache/cassandra/db/KeyspaceNotDefinedException.java +++ b/src/java/org/apache/cassandra/db/KeyspaceNotDefinedException.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.db; +import org.apache.cassandra.exceptions.CassandraExceptionCode; import org.apache.cassandra.exceptions.InvalidRequestException; public class KeyspaceNotDefinedException extends InvalidRequestException @@ -25,4 +26,10 @@ public class KeyspaceNotDefinedException extends InvalidRequestException { super(why); } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.KEYSPACE_NOT_DEFINED; + } } diff --git a/src/java/org/apache/cassandra/db/Mutation.java b/src/java/org/apache/cassandra/db/Mutation.java index e065bb9565..9c4bb8a1e3 100644 --- a/src/java/org/apache/cassandra/db/Mutation.java +++ b/src/java/org/apache/cassandra/db/Mutation.java @@ -30,6 +30,7 @@ import java.util.concurrent.atomic.AtomicLongFieldUpdater; import java.util.function.Predicate; import java.util.function.Supplier; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import com.google.common.base.Preconditions; @@ -58,6 +59,7 @@ import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.AbstractWriteResponseHandler; +import org.apache.cassandra.service.paxos.Commit.Commitable; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.concurrent.Future; @@ -69,7 +71,7 @@ import static org.apache.cassandra.net.MessagingService.VERSION_60; import static org.apache.cassandra.net.MessagingService.VERSION_61; import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime; -public class Mutation implements IMutation, Supplier +public class Mutation implements IMutation, Supplier, Commitable { public static final MutationSerializer serializer = new MutationSerializer(); public static final int ALLOW_POTENTIAL_TRANSACTION_CONFLICTS = 0x01; @@ -215,6 +217,13 @@ public class Mutation implements IMutation, Supplier return modifications.values(); } + public @Nonnull PartitionUpdate getOnlyUpdate() + { + checkState(modifications.size() == 1, "Should only have one PartitionUpdate"); + //noinspection ConstantConditions + return modifications().values().iterator().next(); + } + public long getApproxCreatedAtNanos() { return approxCreatedAtNanos; @@ -468,6 +477,12 @@ public class Mutation implements IMutation, Supplier return new SimpleBuilders.MutationBuilder(mutationId, keyspaceName, partitionKey); } + @Override + public CommitableKind commitableKind() + { + return CommitableKind.MUTATION; + } + /** * Interface for building mutations geared towards human. *

@@ -685,10 +700,10 @@ public class Mutation implements IMutation, Supplier public TableId deserializeTableId(DataInputBuffer in, int version, DeserializationHelper.Flag flag) throws IOException { - if (version >= VERSION_51) + if (version >= VERSION_60) in.skipBytes(1); // flags - if (version >= VERSION_52) + if (version >= VERSION_61) MutationId.serializer.skip(in, version); int size = in.readUnsignedVInt32(); diff --git a/src/java/org/apache/cassandra/db/MutationExceededMaxSizeException.java b/src/java/org/apache/cassandra/db/MutationExceededMaxSizeException.java index 45d9e43505..62b08cb9a2 100644 --- a/src/java/org/apache/cassandra/db/MutationExceededMaxSizeException.java +++ b/src/java/org/apache/cassandra/db/MutationExceededMaxSizeException.java @@ -21,11 +21,15 @@ package org.apache.cassandra.db; import java.util.Iterator; import java.util.List; import java.util.stream.Collectors; +import java.io.IOException; import com.google.common.annotations.VisibleForTesting; import org.apache.cassandra.db.partitions.PartitionUpdate; +import org.apache.cassandra.exceptions.CassandraExceptionCode; import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; import static org.apache.cassandra.db.IMutation.MAX_MUTATION_SIZE; @@ -41,6 +45,13 @@ public class MutationExceededMaxSizeException extends InvalidRequestException this.mutationSize = totalSize; } + // Constructor for deserialization + public MutationExceededMaxSizeException(String message, long mutationSize) + { + super(message); + this.mutationSize = mutationSize; + } + private static String prepareMessage(IMutation mutation, int version, long totalSize) { List topPartitions = mutation.getPartitionUpdates().stream() @@ -87,4 +98,28 @@ public class MutationExceededMaxSizeException extends InvalidRequestException return stringBuilder.toString(); } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.MUTATION_EXCEEDED_MAX_SIZE; + } + + @Override + protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException + { + out.writeUnsignedVInt(mutationSize); + } + + @Override + protected long serializedSizeSpecificFields(int version) + { + return TypeSizes.sizeofUnsignedVInt(mutationSize); + } + + public static MutationExceededMaxSizeException deserializeFields(String message, DataInputPlus in, int version) throws IOException + { + long mutationSize = in.readUnsignedVInt(); + return new MutationExceededMaxSizeException(message, mutationSize); + } } diff --git a/src/java/org/apache/cassandra/db/ReadKind.java b/src/java/org/apache/cassandra/db/ReadKind.java new file mode 100644 index 0000000000..4f05a5ec67 --- /dev/null +++ b/src/java/org/apache/cassandra/db/ReadKind.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db; + +import java.io.IOException; + +import org.apache.cassandra.io.UnversionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; + +public enum ReadKind +{ + UNTRACKED (0), + TRACKED_DATA (1), + TRACKED_SUMMARY (2); + + public final int id; + + ReadKind(int id) + { + this.id = id; + } + + public boolean isTracked() + { + return this != UNTRACKED; + } + + private static final ReadKind[] idToKindMapping; + static + { + int maxId = -1; + for (ReadKind kind : ReadKind.values()) + maxId = Math.max(maxId, kind.id); + idToKindMapping = new ReadKind[maxId + 1]; + for (ReadKind kind : ReadKind.values()) + idToKindMapping[kind.id] = kind; + } + + public static ReadKind fromId(int id) + { + if (id < 0 || id >= idToKindMapping.length || idToKindMapping[id] == null) + throw new IllegalArgumentException("Unknown Kind id: " + id); + return idToKindMapping[id]; + } + + public static final UnversionedSerializer serializer = new UnversionedSerializer<>() + { + @Override + public void serialize(ReadKind kind, DataOutputPlus out) throws IOException + { + out.writeByte(kind.id); + } + + @Override + public ReadKind deserialize(DataInputPlus in) throws IOException + { + return fromId(in.readUnsignedByte()); + } + + @Override + public long serializedSize(ReadKind kind) + { + return TypeSizes.BYTE_SIZE; + } + }; +} diff --git a/src/java/org/apache/cassandra/db/ReadResponse.java b/src/java/org/apache/cassandra/db/ReadResponse.java index 65e17a6920..37691be52e 100644 --- a/src/java/org/apache/cassandra/db/ReadResponse.java +++ b/src/java/org/apache/cassandra/db/ReadResponse.java @@ -39,9 +39,10 @@ import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.utils.ByteBufferUtil; +import static org.apache.cassandra.db.ReadKind.UNTRACKED; import static org.apache.cassandra.db.RepairedDataInfo.NO_OP_REPAIRED_DATA_INFO; -public abstract class ReadResponse +public abstract class ReadResponse implements IReadResponse { // Serializer for single partition read response public static final IVersionedSerializer serializer = new Serializer(); @@ -115,6 +116,12 @@ public abstract class ReadResponse key, ByteBufferUtil.bytesToHex(repairedDataDigest()), isRepairedDigestConclusive()); } + @Override + public ReadKind kind() + { + return UNTRACKED; + } + private String toDebugString(UnfilteredRowIterator partition, TableMetadata metadata) { StringBuilder sb = new StringBuilder(); diff --git a/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java b/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java index 4563245d6f..8867197dfc 100644 --- a/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java +++ b/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java @@ -97,10 +97,12 @@ import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.NoSpamLogger; import org.apache.cassandra.utils.btree.BTreeSet; +import static org.apache.cassandra.db.ReadKind.UNTRACKED; + /** * A read command that selects a (part of a) single partition. */ -public class SinglePartitionReadCommand extends ReadCommand implements SinglePartitionReadQuery +public class SinglePartitionReadCommand extends ReadCommand implements SinglePartitionReadQuery, EmbeddableSinglePartitionReadCommand { private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 1L, TimeUnit.SECONDS); protected static final SelectionDeserializer selectionDeserializer = new Deserializer(); @@ -125,7 +127,7 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar boolean trackWarnings, DataRange dataRange) { - super(serializedAtEpoch, Kind.SINGLE_PARTITION, isDigest, digestVersion, potentialTxnConflicts, metadata, nowInSec, columnFilter, rowFilter, limits, indexQueryPlan, trackWarnings, dataRange); + super(serializedAtEpoch, ReadCommand.Kind.SINGLE_PARTITION, isDigest, digestVersion, potentialTxnConflicts, metadata, nowInSec, columnFilter, rowFilter, limits, indexQueryPlan, trackWarnings, dataRange); assert partitionKey.getPartitioner() == metadata.partitioner; this.partitionKey = partitionKey; this.clusteringIndexFilter = clusteringIndexFilter; @@ -1375,6 +1377,12 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar return false; } + @Override + public ReadKind kind() + { + return UNTRACKED; + } + /* * 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 diff --git a/src/java/org/apache/cassandra/db/SystemKeyspace.java b/src/java/org/apache/cassandra/db/SystemKeyspace.java index 23438dbf83..3cea8e3079 100644 --- a/src/java/org/apache/cassandra/db/SystemKeyspace.java +++ b/src/java/org/apache/cassandra/db/SystemKeyspace.java @@ -261,6 +261,7 @@ public final class SystemKeyspace + "in_progress_read_ballot timeuuid," + "most_recent_commit blob," + "most_recent_commit_at timeuuid," + + "most_recent_commit_mutation_id blob," + "most_recent_commit_version int," + "proposal blob," + "proposal_ballot timeuuid," @@ -1584,7 +1585,7 @@ public final class SystemKeyspace if (proposal instanceof AcceptedWithTTL) { long localDeletionTime = ((Commit.AcceptedWithTTL) proposal).localDeletionTime; - int ttlInSec = legacyPaxosTtlSec(proposal.update.metadata()); + int ttlInSec = legacyPaxosTtlSec(proposal.metadata()); long nowInSec = localDeletionTime - ttlInSec; String cql = "UPDATE system." + PAXOS + " USING TIMESTAMP ? AND TTL ? SET proposal_ballot = ?, proposal = ?, proposal_version = ? WHERE row_key = ? AND cf_id = ?"; executeInternalWithNowInSec(cql, @@ -1594,8 +1595,8 @@ public final class SystemKeyspace proposal.ballot, PartitionUpdate.toBytes(proposal.update, MessagingService.current_version), MessagingService.current_version, - proposal.update.partitionKey().getKey(), - proposal.update.metadata().id.asUUID()); + proposal.partitionKey().getKey(), + proposal.metadata().id.asUUID()); } else { @@ -1605,8 +1606,8 @@ public final class SystemKeyspace proposal.ballot, PartitionUpdate.toBytes(proposal.update, MessagingService.current_version), MessagingService.current_version, - proposal.update.partitionKey().getKey(), - proposal.update.metadata().id.asUUID()); + proposal.partitionKey().getKey(), + proposal.metadata().id.asUUID()); } } @@ -1614,12 +1615,14 @@ public final class SystemKeyspace { // We always erase the last proposal (with the commit timestamp to no erase more recent proposal in case the commit is old) // even though that's really just an optimization since SP.beginAndRepairPaxos will exclude accepted proposal older than the mrc. + ByteBuffer mutationIdBytes = commit.mutation.id().isNone() ? null : commit.mutation.id().toByteBuffer(); + if (commit instanceof Commit.CommittedWithTTL) { long localDeletionTime = ((Commit.CommittedWithTTL) commit).localDeletionTime; - int ttlInSec = legacyPaxosTtlSec(commit.update.metadata()); + int ttlInSec = legacyPaxosTtlSec(commit.metadata()); long nowInSec = localDeletionTime - ttlInSec; - String cql = "UPDATE system." + PAXOS + " USING TIMESTAMP ? AND TTL ? SET proposal_ballot = null, proposal = null, proposal_version = null, most_recent_commit_at = ?, most_recent_commit = ?, most_recent_commit_version = ? WHERE row_key = ? AND cf_id = ?"; + String cql = "UPDATE system." + PAXOS + " USING TIMESTAMP ? AND TTL ? SET proposal_ballot = null, proposal = null, proposal_version = null, most_recent_commit_at = ?, most_recent_commit = ?, most_recent_commit_version = ?, most_recent_commit_mutation_id = ? WHERE row_key = ? AND cf_id = ?"; executeInternalWithNowInSec(cql, nowInSec, commit.ballot.unixMicros(), @@ -1627,19 +1630,21 @@ public final class SystemKeyspace commit.ballot, PartitionUpdate.toBytes(commit.update, MessagingService.current_version), MessagingService.current_version, - commit.update.partitionKey().getKey(), - commit.update.metadata().id.asUUID()); + mutationIdBytes, + commit.partitionKey().getKey(), + commit.metadata().id.asUUID()); } else { - String cql = "UPDATE system." + PAXOS + " USING TIMESTAMP ? SET proposal_ballot = null, proposal = null, proposal_version = null, most_recent_commit_at = ?, most_recent_commit = ?, most_recent_commit_version = ? WHERE row_key = ? AND cf_id = ?"; + String cql = "UPDATE system." + PAXOS + " USING TIMESTAMP ? SET proposal_ballot = null, proposal = null, proposal_version = null, most_recent_commit_at = ?, most_recent_commit = ?, most_recent_commit_version = ?, most_recent_commit_mutation_id = ? WHERE row_key = ? AND cf_id = ?"; executeInternal(cql, commit.ballot.unixMicros(), commit.ballot, PartitionUpdate.toBytes(commit.update, MessagingService.current_version), MessagingService.current_version, - commit.update.partitionKey().getKey(), - commit.update.metadata().id.asUUID()); + mutationIdBytes, + commit.partitionKey().getKey(), + commit.metadata().id.asUUID()); } } diff --git a/src/java/org/apache/cassandra/db/WriteType.java b/src/java/org/apache/cassandra/db/WriteType.java index 3d00770465..45b217cee4 100644 --- a/src/java/org/apache/cassandra/db/WriteType.java +++ b/src/java/org/apache/cassandra/db/WriteType.java @@ -17,19 +17,76 @@ */ package org.apache.cassandra.db; +import java.io.IOException; + +import org.apache.cassandra.io.UnversionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; + /** * Identifier for what type of operation timed out. This type is driver facing as a String, but some drivers convert * this to an enum, meaning any changes to this type require protocol changes and driver support. */ public enum WriteType { - SIMPLE, - BATCH, - UNLOGGED_BATCH, - COUNTER, - BATCH_LOG, - CAS, - VIEW, - CDC + SIMPLE (0), + BATCH (1), + UNLOGGED_BATCH (2), + COUNTER (3), + BATCH_LOG (4), + CAS (5), + VIEW (6), + CDC (7); //TODO update client protocol to support "TRANSACTION" + + // used by the messaging service + public final int id; + + private static final WriteType[] idToTypeMapping; + static + { + int maxId = -1; + for (WriteType type : WriteType.values()) + maxId = Math.max(maxId, type.id); + idToTypeMapping = new WriteType[maxId + 1]; + for (WriteType type : WriteType.values()) + { + if (idToTypeMapping[type.id] != null) + throw new IllegalStateException("Duplicate id " + type.id); + idToTypeMapping[type.id] = type; + } + } + + WriteType(int id) + { + this.id = id; + } + + public static WriteType fromId(int id) throws IOException + { + if (id < 0 || id >= idToTypeMapping.length) + throw new IllegalArgumentException("Unknown WriteType id: " + id); + return idToTypeMapping[id]; + } + + public static final UnversionedSerializer serializer = new UnversionedSerializer<>() + { + @Override + public void serialize(WriteType writeType, DataOutputPlus out) throws IOException + { + out.writeByte(writeType.id); + } + + @Override + public WriteType deserialize(DataInputPlus in) throws IOException + { + return fromId(in.readUnsignedByte()); + } + + @Override + public long serializedSize(WriteType writeType) + { + return TypeSizes.BYTE_SIZE; + } + }; } diff --git a/src/java/org/apache/cassandra/db/guardrails/GuardrailViolatedException.java b/src/java/org/apache/cassandra/db/guardrails/GuardrailViolatedException.java index e8a47951a1..75cad867b6 100644 --- a/src/java/org/apache/cassandra/db/guardrails/GuardrailViolatedException.java +++ b/src/java/org/apache/cassandra/db/guardrails/GuardrailViolatedException.java @@ -18,12 +18,24 @@ package org.apache.cassandra.db.guardrails; +import org.apache.cassandra.exceptions.CassandraExceptionCode; import org.apache.cassandra.exceptions.InvalidRequestException; public class GuardrailViolatedException extends InvalidRequestException { - GuardrailViolatedException(String message) + public GuardrailViolatedException(String message) { super(message); } + + protected GuardrailViolatedException(String message, Throwable cause) + { + super(message, cause); + } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.GUARDRAIL_VIOLATED; + } } diff --git a/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java b/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java index d915f26c77..a65d71cfcb 100644 --- a/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java +++ b/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java @@ -81,6 +81,7 @@ import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.accord.api.PartitionKey; import org.apache.cassandra.service.accord.serializers.TableMetadatas; +import org.apache.cassandra.service.paxos.Commit.Commitable; import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.utils.Pair; @@ -106,7 +107,7 @@ import static org.apache.cassandra.db.rows.UnfilteredRowIteratorSerializer.IS_EM * is also a few static helper constructor methods for special cases ({@code emptyUpdate()}, * {@code fullPartitionDelete} and {@code singleRowUpdate}). */ -public class PartitionUpdate extends AbstractBTreePartition +public class PartitionUpdate extends AbstractBTreePartition implements Commitable { protected static final Logger logger = LoggerFactory.getLogger(PartitionUpdate.class); @@ -618,6 +619,12 @@ public class PartitionUpdate extends AbstractBTreePartition return super.equals(obj); } + @Override + public CommitableKind commitableKind() + { + return CommitableKind.PARTITION_UPDATE; + } + /** * Interface for building partition updates geared towards human. *

diff --git a/src/java/org/apache/cassandra/exceptions/AlreadyExistsException.java b/src/java/org/apache/cassandra/exceptions/AlreadyExistsException.java index 1829c5cb1f..9f144f577d 100644 --- a/src/java/org/apache/cassandra/exceptions/AlreadyExistsException.java +++ b/src/java/org/apache/cassandra/exceptions/AlreadyExistsException.java @@ -17,6 +17,12 @@ */ package org.apache.cassandra.exceptions; +import java.io.IOException; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; + public class AlreadyExistsException extends ConfigurationException { public final String ksName; @@ -39,4 +45,29 @@ public class AlreadyExistsException extends ConfigurationException this(ksName, "", String.format("Cannot add existing keyspace \"%s\"", ksName)); } + @Override + protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException + { + out.writeUTF(ksName); + out.writeUTF(cfName); + } + + @Override + protected long serializedSizeSpecificFields(int version) + { + return TypeSizes.sizeof(ksName) + TypeSizes.sizeof(cfName); + } + + static AlreadyExistsException deserializeFields(String message, DataInputPlus in, int version) throws IOException + { + String ksName = in.readUTF(); + String cfName = in.readUTF(); + return new AlreadyExistsException(ksName, cfName, message); + } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.ALREADY_EXISTS; + } } diff --git a/src/java/org/apache/cassandra/exceptions/AuthenticationException.java b/src/java/org/apache/cassandra/exceptions/AuthenticationException.java index 067f3ae2f4..cf3f4119ec 100644 --- a/src/java/org/apache/cassandra/exceptions/AuthenticationException.java +++ b/src/java/org/apache/cassandra/exceptions/AuthenticationException.java @@ -28,4 +28,10 @@ public class AuthenticationException extends RequestValidationException { super(ExceptionCode.BAD_CREDENTIALS, msg, e); } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.BAD_CREDENTIALS; + } } diff --git a/src/java/org/apache/cassandra/exceptions/CDCWriteException.java b/src/java/org/apache/cassandra/exceptions/CDCWriteException.java index d60c1d3efd..0dfc9425e0 100644 --- a/src/java/org/apache/cassandra/exceptions/CDCWriteException.java +++ b/src/java/org/apache/cassandra/exceptions/CDCWriteException.java @@ -23,4 +23,10 @@ public class CDCWriteException extends RequestExecutionException { super(ExceptionCode.CDC_WRITE_FAILURE, msg); } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.CDC_WRITE_FAILURE; + } } diff --git a/src/java/org/apache/cassandra/exceptions/CasWriteTimeoutException.java b/src/java/org/apache/cassandra/exceptions/CasWriteTimeoutException.java index 32cc014da1..bfe7fd6661 100644 --- a/src/java/org/apache/cassandra/exceptions/CasWriteTimeoutException.java +++ b/src/java/org/apache/cassandra/exceptions/CasWriteTimeoutException.java @@ -17,9 +17,13 @@ */ package org.apache.cassandra.exceptions; -import org.apache.cassandra.db.ConsistencyLevel; -import org.apache.cassandra.db.WriteType; +import java.io.IOException; +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.db.WriteType; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; public class CasWriteTimeoutException extends WriteTimeoutException { @@ -30,4 +34,33 @@ public class CasWriteTimeoutException extends WriteTimeoutException super(writeType, consistency, received, blockFor, String.format("CAS operation timed out: received %d of %d required responses after %d contention retries", received, blockFor, contentions)); this.contentions = contentions; } + + @Override + protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException + { + super.serializeSpecificFields(out, version); + out.writeUnsignedVInt32(contentions); + } + + @Override + protected long serializedSizeSpecificFields(int version) + { + return super.serializedSizeSpecificFields(version) + TypeSizes.sizeofUnsignedVInt(contentions); + } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.CAS_WRITE_TIMEOUT; + } + + static CasWriteTimeoutException deserializeFields(String message, DataInputPlus in, int version) throws IOException + { + ConsistencyLevel consistency = ConsistencyLevel.fromCode(in.readUnsignedByte()); + int received = in.readUnsignedVInt32(); + int blockFor = in.readUnsignedVInt32(); + WriteType writeType = WriteType.serializer.deserialize(in); + int contentions = in.readUnsignedVInt32(); + return new CasWriteTimeoutException(writeType, consistency, received, blockFor, contentions); + } } diff --git a/src/java/org/apache/cassandra/exceptions/CasWriteUnknownResultException.java b/src/java/org/apache/cassandra/exceptions/CasWriteUnknownResultException.java index d5dda8455e..a452069c7b 100644 --- a/src/java/org/apache/cassandra/exceptions/CasWriteUnknownResultException.java +++ b/src/java/org/apache/cassandra/exceptions/CasWriteUnknownResultException.java @@ -18,7 +18,12 @@ package org.apache.cassandra.exceptions; +import java.io.IOException; + import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; public class CasWriteUnknownResultException extends RequestExecutionException { @@ -33,4 +38,34 @@ public class CasWriteUnknownResultException extends RequestExecutionException this.received = received; this.blockFor = blockFor; } + + @Override + protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException + { + out.writeByte(consistency.code); + out.writeUnsignedVInt32(received); + out.writeUnsignedVInt32(blockFor); + } + + @Override + protected long serializedSizeSpecificFields(int version) + { + return TypeSizes.BYTE_SIZE + // consistency + TypeSizes.sizeofUnsignedVInt(received) + + TypeSizes.sizeofUnsignedVInt(blockFor); + } + + static CasWriteUnknownResultException deserializeFields(String message, DataInputPlus in, int version) throws IOException + { + ConsistencyLevel consistency = ConsistencyLevel.fromCode(in.readUnsignedByte()); + int received = in.readUnsignedVInt32(); + int blockFor = in.readUnsignedVInt32(); + return new CasWriteUnknownResultException(consistency, received, blockFor); + } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.CAS_WRITE_UNKNOWN; + } } diff --git a/src/java/org/apache/cassandra/exceptions/CassandraException.java b/src/java/org/apache/cassandra/exceptions/CassandraException.java index 119daac22f..88d31310a8 100644 --- a/src/java/org/apache/cassandra/exceptions/CassandraException.java +++ b/src/java/org/apache/cassandra/exceptions/CassandraException.java @@ -17,6 +17,23 @@ */ package org.apache.cassandra.exceptions; +import java.io.IOException; + +import org.apache.cassandra.cql3.constraints.ConstraintViolationException; +import org.apache.cassandra.cql3.constraints.InvalidConstraintDefinitionException; +import org.apache.cassandra.db.KeyspaceNotDefinedException; +import org.apache.cassandra.db.MutationExceededMaxSizeException; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.db.guardrails.GuardrailViolatedException; +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.exceptions.AccordReadExhaustedException; +import org.apache.cassandra.service.accord.exceptions.AccordReadPreemptedException; +import org.apache.cassandra.service.accord.exceptions.AccordWriteExhaustedException; +import org.apache.cassandra.service.accord.exceptions.AccordWritePreemptedException; +import org.apache.cassandra.triggers.TriggerDisabledException; +import org.apache.cassandra.utils.ArraySerializers; import org.apache.cassandra.utils.Shared; import static org.apache.cassandra.utils.Shared.Scope.SIMULATION; @@ -24,22 +41,180 @@ import static org.apache.cassandra.utils.Shared.Scope.SIMULATION; @Shared(scope = SIMULATION) public abstract class CassandraException extends RuntimeException implements TransportException { - private final ExceptionCode code; + public static final Serializer serializer = new Serializer(); + + private final ExceptionCode clientExceptionCode; protected CassandraException(ExceptionCode code, String msg) { super(msg); - this.code = code; + this.clientExceptionCode = code; } protected CassandraException(ExceptionCode code, String msg, Throwable cause) { super(msg, cause); - this.code = code; + this.clientExceptionCode = code; } public ExceptionCode code() { - return code; + return clientExceptionCode; + } + + /** + * Returns the CassandraExceptionCode for this specific exception class. + * Each subclass must implement this to return its unique serialization code. + */ + public abstract CassandraExceptionCode getCassandraExceptionCode(); + + /** + * Serializer for CassandraException that coordinates serialization of subclasses. + */ + @Shared(scope = SIMULATION) + public static class Serializer implements IVersionedSerializer + { + @Override + public void serialize(CassandraException exception, DataOutputPlus out, int version) throws IOException + { + String message = exception.getMessage() == null ? "" : exception.getMessage(); + out.writeUnsignedVInt32(exception.getCassandraExceptionCode().value); + out.writeUTF(message); + ExceptionSerializer.nullableRemoteExceptionSerializer.serialize(exception.getCause(), out, version); + ArraySerializers.serializeArray(exception.getSuppressed(), out, version, ExceptionSerializer.remoteExceptionSerializer); + StackTraceElement[] stackTrace = exception.getStackTrace(); + ArraySerializers.serializeArray(stackTrace, out, version, ExceptionSerializer.stackTraceElementSerializer); + // delegate to subclass serializer for type-specific fields + exception.serializeSpecificFields(out, version); + } + + @Override + public CassandraException deserialize(DataInputPlus in, int version) throws IOException + { + CassandraExceptionCode classCode = CassandraExceptionCode.fromValue(in.readUnsignedVInt32()); + String message = in.readUTF(); + Throwable cause = ExceptionSerializer.nullableRemoteExceptionSerializer.deserialize(in, version); + Throwable[] suppressed = ArraySerializers.deserializeArray(in, version, ExceptionSerializer.remoteExceptionSerializer, Throwable[]::new); + StackTraceElement[] stackTrace = ArraySerializers.deserializeArray(in, version, ExceptionSerializer.stackTraceElementSerializer, StackTraceElement[]::new); + // delegate to subclass serializer for type-specific fields + CassandraException exception = deserializeByClassCode(classCode, message, in, version); + + if (cause != null) + exception.initCause(cause); + for (Throwable t : suppressed) + exception.addSuppressed(t); + exception.setStackTrace(stackTrace); + + return exception; + } + + @Override + public long serializedSize(CassandraException exception, int version) + { + String message = exception.getMessage() == null ? "" : exception.getMessage(); + long size = TypeSizes.sizeofUnsignedVInt(exception.getCassandraExceptionCode().value); + size += TypeSizes.sizeof(message); + size += ExceptionSerializer.nullableRemoteExceptionSerializer.serializedSize(exception.getCause(), version); + size += ArraySerializers.serializedArraySize(exception.getSuppressed(), version, ExceptionSerializer.remoteExceptionSerializer); + size += ArraySerializers.serializedArraySize(exception.getStackTrace(), version, ExceptionSerializer.stackTraceElementSerializer); + // delegate to subclass serializer for type-specific fields + size += exception.serializedSizeSpecificFields(version); + return size; + } + + private CassandraException deserializeByClassCode(CassandraExceptionCode cassandraExceptionCode, String message, DataInputPlus in, int version) throws IOException + { + // Create the appropriate exception instance based on the class code + switch (cassandraExceptionCode) + { + case UNAVAILABLE: + return UnavailableException.deserializeFields(message, in, version); + case FUNCTION_FAILURE: + return FunctionExecutionException.deserializeFields(message, in, version); + case OPERATION_EXECUTION: + return OperationExecutionException.deserializeFields(message, in, version); + case OVERLOADED: + return new OverloadedException(message); + case CAS_WRITE_UNKNOWN: + return CasWriteUnknownResultException.deserializeFields(message, in, version); + case IS_BOOTSTRAPPING: + return new IsBootstrappingException(); + case CDC_WRITE_FAILURE: + return new CDCWriteException(message); + case TRUNCATE_ERROR: + return new TruncateException(message); + case READ_TIMEOUT: + return ReadTimeoutException.deserializeFields(message, in, version); + case ACCORD_READ_EXHAUSTED: + return AccordReadExhaustedException.deserializeFields(message, in, version); + case ACCORD_READ_PREEMPTED: + return AccordReadPreemptedException.deserializeFields(message, in, version); + case ACCORD_WRITE_PREEMPTED: + return AccordWritePreemptedException.deserializeFields(message, in, version); + case ACCORD_WRITE_EXHAUSTED: + return AccordWriteExhaustedException.deserializeFields(message, in, version); + case WRITE_TIMEOUT: + return WriteTimeoutException.deserializeFields(message, in, version); + case CAS_WRITE_TIMEOUT: + return CasWriteTimeoutException.deserializeFields(message, in, version); + case READ_FAILURE: + return ReadFailureException.deserializeFields(message, in, version); + case TOMBSTONE_ABORT: + return TombstoneAbortException.deserializeFields(message, in, version); + case READ_SIZE_ABORT: + return ReadSizeAbortException.deserializeFields(message, in, version); + case QUERY_TOO_MANY_INDEXES_ABORT: + return QueryReferencesTooManyIndexesAbortException.deserializeFields(message, in, version); + case WRITE_FAILURE: + return WriteFailureException.deserializeFields(message, in, version); + case UNPREPARED: + return PreparedQueryNotFoundException.deserializeFields(message, in, version); + case BAD_CREDENTIALS: + return new AuthenticationException(message); + case CONFIG_ERROR: + return new ConfigurationException(message); + case ALREADY_EXISTS: + return AlreadyExistsException.deserializeFields(message, in, version); + case UNAUTHORIZED: + return new UnauthorizedException(message); + case INVALID: + return new InvalidRequestException(message); + case INVALID_CONSTRAINT_DEFINITION: + return new InvalidConstraintDefinitionException(message); + case CONSTRAINT_VIOLATION: + return new ConstraintViolationException(message); + case TRIGGER_DISABLED: + return new TriggerDisabledException(message); + case KEYSPACE_NOT_DEFINED: + return new KeyspaceNotDefinedException(message); + case GUARDRAIL_VIOLATED: + return new GuardrailViolatedException(message); + case MUTATION_EXCEEDED_MAX_SIZE: + return MutationExceededMaxSizeException.deserializeFields(message, in, version); + case OVERSIZED_MESSAGE: + return new OversizedCQLMessageException(message); + case INVALID_ROUTING: + return new InvalidRoutingException(message); + case SYNTAX_ERROR: + return new SyntaxException(message); + default: + throw new AssertionError("Unhandled CassandraExceptionCode: " + cassandraExceptionCode); + } + } + } + + /** + * Serialize subclass-specific fields. Override in subclasses that have additional fields. + */ + protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException + { + } + + /** + * Calculate serialized size of subclass-specific fields. Override in subclasses that have additional fields. + */ + protected long serializedSizeSpecificFields(int version) + { + return 0; } } diff --git a/src/java/org/apache/cassandra/exceptions/CassandraExceptionCode.java b/src/java/org/apache/cassandra/exceptions/CassandraExceptionCode.java new file mode 100644 index 0000000000..9eb367463b --- /dev/null +++ b/src/java/org/apache/cassandra/exceptions/CassandraExceptionCode.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.exceptions; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.cassandra.utils.Shared; + +import static org.apache.cassandra.utils.Shared.Scope.SIMULATION; + +/** + * Exception codes for internal CassandraException serialization. + * Each concrete CassandraException subclass has a unique code to ensure proper type fidelity during serialization. + * This is separate from the client-facing ExceptionCode enum which maintains backward compatibility. + */ +@Shared(scope = SIMULATION) +public enum CassandraExceptionCode +{ + // Execution exceptions + UNAVAILABLE (0), // UnavailableException + FUNCTION_FAILURE (1), // FunctionExecutionException + OPERATION_EXECUTION (2), // OperationExecutionException + OVERLOADED (3), // OverloadedException + CAS_WRITE_UNKNOWN (4), // CasWriteUnknownResultException + IS_BOOTSTRAPPING (5), // IsBootstrappingException + CDC_WRITE_FAILURE (6), // CDCWriteException + TRUNCATE_ERROR (7), // TruncateException + // Timeout exceptions + READ_TIMEOUT (8), // ReadTimeoutException + ACCORD_READ_EXHAUSTED (9), // AccordReadExhaustedException + ACCORD_READ_PREEMPTED (10), // AccordReadPreemptedException + WRITE_TIMEOUT (11), // WriteTimeoutException + ACCORD_WRITE_PREEMPTED (12), // AccordWritePreemptedException + ACCORD_WRITE_EXHAUSTED (13), // AccordWriteExhaustedException + CAS_WRITE_TIMEOUT (14), // CasWriteTimeoutException + // Failure exceptions + READ_FAILURE (15), // ReadFailureException + TOMBSTONE_ABORT (16), // TombstoneAbortException + READ_SIZE_ABORT (17), // ReadSizeAbortException + QUERY_TOO_MANY_INDEXES_ABORT (18), // QueryReferencesTooManyIndexesAbortException + WRITE_FAILURE (19), // WriteFailureException + // Validation/config exceptions + UNPREPARED (20), // PreparedQueryNotFoundException + BAD_CREDENTIALS (21), // AuthenticationException + CONFIG_ERROR (22), // ConfigurationException + ALREADY_EXISTS (23), // AlreadyExistsException + UNAUTHORIZED (24), // UnauthorizedException + INVALID (25), // InvalidRequestException + INVALID_CONSTRAINT_DEFINITION (26), // InvalidConstraintDefinitionException + CONSTRAINT_VIOLATION (27), // ConstraintViolationException + OVERSIZED_MESSAGE (28), // OversizedCQLMessageException + TRIGGER_DISABLED (29), // TriggerDisabledException + KEYSPACE_NOT_DEFINED (30), // KeyspaceNotDefinedException + GUARDRAIL_VIOLATED (31), // GuardrailViolatedException + INVALID_ROUTING (32), // InvalidRoutingException + MUTATION_EXCEEDED_MAX_SIZE (33), // MutationExceededMaxSizeException + SYNTAX_ERROR (34); // SyntaxException + + public final int value; + private static final Map valueToCode = new HashMap<>(CassandraExceptionCode.values().length); + + static + { + for (CassandraExceptionCode code : CassandraExceptionCode.values()) + valueToCode.put(code.value, code); + } + + CassandraExceptionCode(int value) + { + this.value = value; + } + + public static CassandraExceptionCode fromValue(int value) + { + CassandraExceptionCode code = valueToCode.get(value); + if (code == null) + throw new IllegalArgumentException(String.format("Unknown CassandraException code %d", value)); + return code; + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/exceptions/ConfigurationException.java b/src/java/org/apache/cassandra/exceptions/ConfigurationException.java index f28fa516af..d3fa402c75 100644 --- a/src/java/org/apache/cassandra/exceptions/ConfigurationException.java +++ b/src/java/org/apache/cassandra/exceptions/ConfigurationException.java @@ -49,4 +49,10 @@ public class ConfigurationException extends RequestValidationException super(code, msg); logStackTrace = true; } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.CONFIG_ERROR; + } } diff --git a/src/java/org/apache/cassandra/exceptions/ExceptionSerializer.java b/src/java/org/apache/cassandra/exceptions/ExceptionSerializer.java index 834abca49d..c8e9f24ae7 100644 --- a/src/java/org/apache/cassandra/exceptions/ExceptionSerializer.java +++ b/src/java/org/apache/cassandra/exceptions/ExceptionSerializer.java @@ -78,7 +78,7 @@ public class ExceptionSerializer return t.getLocalizedMessage(); } - private static final IVersionedSerializer stackTraceElementSerializer = new IVersionedSerializer() + static final IVersionedSerializer stackTraceElementSerializer = new IVersionedSerializer() { @Override public void serialize(StackTraceElement t, DataOutputPlus out, int version) throws IOException diff --git a/src/java/org/apache/cassandra/exceptions/FunctionExecutionException.java b/src/java/org/apache/cassandra/exceptions/FunctionExecutionException.java index a9a99f607e..109d857054 100644 --- a/src/java/org/apache/cassandra/exceptions/FunctionExecutionException.java +++ b/src/java/org/apache/cassandra/exceptions/FunctionExecutionException.java @@ -17,11 +17,16 @@ */ package org.apache.cassandra.exceptions; +import java.io.IOException; import java.util.List; import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.cql3.functions.FunctionName; +import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.utils.CollectionSerializers; public class FunctionExecutionException extends RequestExecutionException { @@ -50,4 +55,42 @@ public class FunctionExecutionException extends RequestExecutionException this.argTypes = argTypes; this.detail = msg; } + + @Override + protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException + { + out.writeBoolean(functionName.keyspace != null); + if (functionName.keyspace != null) + out.writeUTF(functionName.keyspace); + out.writeUTF(functionName.name); + CollectionSerializers.serializeList(argTypes, out, version, CollectionSerializers.STRING_SERIALIZER); + out.writeUTF(detail); + } + + @Override + protected long serializedSizeSpecificFields(int version) + { + long size = TypeSizes.BOOL_SIZE; // keyspace present flag + if (functionName.keyspace != null) + size += TypeSizes.sizeof(functionName.keyspace); + size += TypeSizes.sizeof(functionName.name); + size += CollectionSerializers.serializedListSize(argTypes, version, CollectionSerializers.STRING_SERIALIZER); + size += TypeSizes.sizeof(detail); + return size; + } + + static FunctionExecutionException deserializeFields(String message, DataInputPlus in, int version) throws IOException + { + String keyspace = in.readBoolean() ? in.readUTF() : null; + String name = in.readUTF(); + List argTypes = CollectionSerializers.deserializeList(in, version, CollectionSerializers.STRING_SERIALIZER); + String detail = in.readUTF(); + return new FunctionExecutionException(new FunctionName(keyspace, name), argTypes, detail); + } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.FUNCTION_FAILURE; + } } diff --git a/src/java/org/apache/cassandra/exceptions/InvalidRequestException.java b/src/java/org/apache/cassandra/exceptions/InvalidRequestException.java index 846c38a4db..f0d27506d8 100644 --- a/src/java/org/apache/cassandra/exceptions/InvalidRequestException.java +++ b/src/java/org/apache/cassandra/exceptions/InvalidRequestException.java @@ -28,4 +28,10 @@ public class InvalidRequestException extends RequestValidationException { super(ExceptionCode.INVALID, msg, t); } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.INVALID; + } } diff --git a/src/java/org/apache/cassandra/exceptions/InvalidRoutingException.java b/src/java/org/apache/cassandra/exceptions/InvalidRoutingException.java index 8fa34d965c..439974675e 100644 --- a/src/java/org/apache/cassandra/exceptions/InvalidRoutingException.java +++ b/src/java/org/apache/cassandra/exceptions/InvalidRoutingException.java @@ -31,7 +31,7 @@ public class InvalidRoutingException extends InvalidRequestException public static final String RANGE_TEMPLATE = "Received a read request from %s for a range [%s,%s] that is not owned by the current replica as of %s: %s."; public static final String WRITE_TEMPLATE = "Received a mutation from %s for a token %s that is not owned by the current replica as of %s: %s."; - private InvalidRoutingException(String msg) + public InvalidRoutingException(String msg) { super(msg); } @@ -59,4 +59,10 @@ public class InvalidRoutingException extends InvalidRequestException { return new InvalidRoutingException(String.format(WRITE_TEMPLATE, from, token, epoch, mutation.getKeyspaceName())); } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.INVALID_ROUTING; + } } diff --git a/src/java/org/apache/cassandra/exceptions/IsBootstrappingException.java b/src/java/org/apache/cassandra/exceptions/IsBootstrappingException.java index b25a3184d7..17307dc479 100644 --- a/src/java/org/apache/cassandra/exceptions/IsBootstrappingException.java +++ b/src/java/org/apache/cassandra/exceptions/IsBootstrappingException.java @@ -23,4 +23,10 @@ public class IsBootstrappingException extends RequestExecutionException { super(ExceptionCode.IS_BOOTSTRAPPING, "Cannot read from a bootstrapping node"); } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.IS_BOOTSTRAPPING; + } } diff --git a/src/java/org/apache/cassandra/exceptions/OperationExecutionException.java b/src/java/org/apache/cassandra/exceptions/OperationExecutionException.java index f86969e472..7c58c0e70c 100644 --- a/src/java/org/apache/cassandra/exceptions/OperationExecutionException.java +++ b/src/java/org/apache/cassandra/exceptions/OperationExecutionException.java @@ -17,10 +17,12 @@ */ package org.apache.cassandra.exceptions; +import java.io.IOException; import java.util.List; import org.apache.cassandra.cql3.functions.OperationFcts; import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.io.util.DataInputPlus; /** * Thrown when an operation problem has occured (e.g. division by zero with integer). @@ -54,4 +56,22 @@ public final class OperationExecutionException extends FunctionExecutionExceptio super(OperationFcts.getFunctionNameFromOperator(operator), argTypes, msg); } + static OperationExecutionException deserializeFields(String message, DataInputPlus in, int version) throws IOException + { + // Deserialize using parent's logic to get FunctionName, argTypes, and detail + FunctionExecutionException parent = FunctionExecutionException.deserializeFields(message, in, version); + + // Extract operator from function name using OperationFcts + char operator = OperationFcts.getOperator(parent.functionName); + + // Create OperationExecutionException with the extracted operator + return new OperationExecutionException(operator, parent.argTypes, parent.detail); + } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.OPERATION_EXECUTION; + } + } diff --git a/src/java/org/apache/cassandra/exceptions/OverloadedException.java b/src/java/org/apache/cassandra/exceptions/OverloadedException.java index 0235a535c4..c2732bed78 100644 --- a/src/java/org/apache/cassandra/exceptions/OverloadedException.java +++ b/src/java/org/apache/cassandra/exceptions/OverloadedException.java @@ -23,4 +23,10 @@ public class OverloadedException extends RequestExecutionException { super(ExceptionCode.OVERLOADED, reason); } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.OVERLOADED; + } } diff --git a/src/java/org/apache/cassandra/exceptions/OversizedCQLMessageException.java b/src/java/org/apache/cassandra/exceptions/OversizedCQLMessageException.java index 60f9c48835..1b20ba3466 100644 --- a/src/java/org/apache/cassandra/exceptions/OversizedCQLMessageException.java +++ b/src/java/org/apache/cassandra/exceptions/OversizedCQLMessageException.java @@ -24,4 +24,10 @@ public class OversizedCQLMessageException extends InvalidRequestException { super(message); } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.OVERSIZED_MESSAGE; + } } diff --git a/src/java/org/apache/cassandra/exceptions/PreparedQueryNotFoundException.java b/src/java/org/apache/cassandra/exceptions/PreparedQueryNotFoundException.java index a43f83e5ce..2556362ed7 100644 --- a/src/java/org/apache/cassandra/exceptions/PreparedQueryNotFoundException.java +++ b/src/java/org/apache/cassandra/exceptions/PreparedQueryNotFoundException.java @@ -17,6 +17,10 @@ */ package org.apache.cassandra.exceptions; +import java.io.IOException; + +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.utils.MD5Digest; public class PreparedQueryNotFoundException extends RequestValidationException @@ -36,4 +40,31 @@ public class PreparedQueryNotFoundException extends RequestValidationException " or you have prepared too many queries and it has been evicted from the internal cache)", id); } + + @Override + protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException + { + // MD5 digest is always 16 bytes + out.write(id.bytes); + } + + @Override + protected long serializedSizeSpecificFields(int version) + { + return id.bytes.length; // MD5 is always 16 bytes + } + + static PreparedQueryNotFoundException deserializeFields(String message, DataInputPlus in, int version) throws IOException + { + byte[] bytes = new byte[16]; // MD5 is always 16 bytes + in.readFully(bytes); + MD5Digest id = MD5Digest.wrap(bytes); + return new PreparedQueryNotFoundException(id); + } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.UNPREPARED; + } } diff --git a/src/java/org/apache/cassandra/exceptions/QueryReferencesTooManyIndexesAbortException.java b/src/java/org/apache/cassandra/exceptions/QueryReferencesTooManyIndexesAbortException.java index d2f5e98ead..b089818c22 100644 --- a/src/java/org/apache/cassandra/exceptions/QueryReferencesTooManyIndexesAbortException.java +++ b/src/java/org/apache/cassandra/exceptions/QueryReferencesTooManyIndexesAbortException.java @@ -18,9 +18,13 @@ package org.apache.cassandra.exceptions; +import java.io.IOException; import java.util.Map; import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.locator.InetAddressAndPort; public class QueryReferencesTooManyIndexesAbortException extends ReadAbortException @@ -34,4 +38,38 @@ public class QueryReferencesTooManyIndexesAbortException extends ReadAbortExcept this.nodes = nodes; this.maxValue = maxValue; } + + @Override + protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException + { + // Serialize parent fields first + super.serializeSpecificFields(out, version); + // Add QueryReferencesTooManyIndexesAbortException specific fields + out.writeUnsignedVInt32(nodes); + out.writeUnsignedVInt(maxValue); + } + + @Override + protected long serializedSizeSpecificFields(int version) + { + return super.serializedSizeSpecificFields(version) + + TypeSizes.sizeofUnsignedVInt(nodes) + + TypeSizes.sizeofUnsignedVInt(maxValue); + } + + static QueryReferencesTooManyIndexesAbortException deserializeFields(String message, DataInputPlus in, int version) throws IOException + { + ReadFailureException.DeserializedFields fields = ReadFailureException.deserializeBaseFields(in, version); + int nodes = in.readUnsignedVInt32(); + long maxValue = in.readUnsignedVInt(); + + return new QueryReferencesTooManyIndexesAbortException(message, nodes, maxValue, fields.dataPresent, + fields.consistency, fields.received, fields.blockFor, fields.failures); + } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.QUERY_TOO_MANY_INDEXES_ABORT; + } } diff --git a/src/java/org/apache/cassandra/exceptions/ReadFailureException.java b/src/java/org/apache/cassandra/exceptions/ReadFailureException.java index 98f0e7e20b..993f854f42 100644 --- a/src/java/org/apache/cassandra/exceptions/ReadFailureException.java +++ b/src/java/org/apache/cassandra/exceptions/ReadFailureException.java @@ -17,12 +17,17 @@ */ package org.apache.cassandra.exceptions; +import java.io.IOException; import java.util.Map; import com.google.common.collect.ImmutableMap; import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.utils.CollectionSerializers; public class ReadFailureException extends RequestFailureException { @@ -51,4 +56,88 @@ public class ReadFailureException extends RequestFailureException super(ExceptionCode.READ_FAILURE, rfe.getMessage(), rfe.consistency, rfe.received, rfe.blockFor, rfe.failureReasonByEndpoint, rfe); this.dataPresent = rfe.dataPresent; } + + @Override + protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException + { + out.writeByte(consistency.code); + out.writeUnsignedVInt32(received); + out.writeUnsignedVInt32(blockFor); + + // Serialize failure reason map + CollectionSerializers.serializeMap(failureReasonByEndpoint, out, version, + InetAddressAndPort.Serializer.inetAddressAndPortSerializer, + RequestFailureReason.serializer); + + out.writeBoolean(dataPresent); + } + + @Override + protected long serializedSizeSpecificFields(int version) + { + long size = TypeSizes.BYTE_SIZE + // consistency + TypeSizes.sizeofUnsignedVInt(received) + + TypeSizes.sizeofUnsignedVInt(blockFor); + + size += CollectionSerializers.serializedMapSize(failureReasonByEndpoint, version, + InetAddressAndPort.Serializer.inetAddressAndPortSerializer, + RequestFailureReason.serializer); + + size += TypeSizes.BOOL_SIZE; // dataPresent + return size; + } + + static ReadFailureException deserializeFields(String message, DataInputPlus in, int version) throws IOException + { + DeserializedFields fields = deserializeBaseFields(in, version); + return new ReadFailureException(message, fields.consistency, fields.received, fields.blockFor, fields.dataPresent, fields.failures); + } + + /** + * Helper class to hold deserialized base fields for subclasses to use. + */ + static class DeserializedFields + { + final ConsistencyLevel consistency; + final int received; + final int blockFor; + final Map failures; + final boolean dataPresent; + + DeserializedFields(ConsistencyLevel consistency, int received, int blockFor, + Map failures, boolean dataPresent) + { + this.consistency = consistency; + this.received = received; + this.blockFor = blockFor; + this.failures = failures; + this.dataPresent = dataPresent; + } + } + + /** + * Deserialize the base fields common to ReadFailureException and its subclasses. + * Subclasses should call this method and then read any additional fields. + */ + static DeserializedFields deserializeBaseFields(DataInputPlus in, int version) throws IOException + { + ConsistencyLevel consistency = ConsistencyLevel.fromCode(in.readUnsignedByte()); + int received = in.readUnsignedVInt32(); + int blockFor = in.readUnsignedVInt32(); + + // Deserialize failure reason map + Map failures = + CollectionSerializers.deserializeMap(in, version, + InetAddressAndPort.Serializer.inetAddressAndPortSerializer, + RequestFailureReason.serializer); + + boolean dataPresent = in.readBoolean(); + return new DeserializedFields(consistency, received, blockFor, failures, dataPresent); + } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.READ_FAILURE; + } } diff --git a/src/java/org/apache/cassandra/exceptions/ReadSizeAbortException.java b/src/java/org/apache/cassandra/exceptions/ReadSizeAbortException.java index ed81008594..0e791e3e01 100644 --- a/src/java/org/apache/cassandra/exceptions/ReadSizeAbortException.java +++ b/src/java/org/apache/cassandra/exceptions/ReadSizeAbortException.java @@ -18,9 +18,11 @@ package org.apache.cassandra.exceptions; +import java.io.IOException; import java.util.Map; import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.locator.InetAddressAndPort; public class ReadSizeAbortException extends ReadAbortException @@ -29,4 +31,16 @@ public class ReadSizeAbortException extends ReadAbortException { super(msg, consistency, received, blockFor, dataPresent, failureReasonByEndpoint); } + + static ReadSizeAbortException deserializeFields(String message, DataInputPlus in, int version) throws IOException + { + ReadFailureException.DeserializedFields fields = ReadFailureException.deserializeBaseFields(in, version); + return new ReadSizeAbortException(message, fields.consistency, fields.received, fields.blockFor, fields.dataPresent, fields.failures); + } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.READ_SIZE_ABORT; + } } diff --git a/src/java/org/apache/cassandra/exceptions/ReadTimeoutException.java b/src/java/org/apache/cassandra/exceptions/ReadTimeoutException.java index 199f165d31..5f33661596 100644 --- a/src/java/org/apache/cassandra/exceptions/ReadTimeoutException.java +++ b/src/java/org/apache/cassandra/exceptions/ReadTimeoutException.java @@ -17,7 +17,12 @@ */ package org.apache.cassandra.exceptions; +import java.io.IOException; + import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; public class ReadTimeoutException extends RequestTimeoutException { @@ -46,4 +51,37 @@ public class ReadTimeoutException extends RequestTimeoutException super(ExceptionCode.READ_TIMEOUT, rfe.consistency, rfe.received, rfe.blockFor, rfe); this.dataPresent = rfe.dataPresent; } + + @Override + protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException + { + out.writeByte(consistency.code); + out.writeUnsignedVInt32(received); + out.writeUnsignedVInt32(blockFor); + out.writeBoolean(dataPresent); + } + + @Override + protected long serializedSizeSpecificFields(int version) + { + return TypeSizes.BYTE_SIZE + // consistency + TypeSizes.sizeofUnsignedVInt(received) + + TypeSizes.sizeofUnsignedVInt(blockFor) + + TypeSizes.BOOL_SIZE; // dataPresent + } + + static ReadTimeoutException deserializeFields(String message, DataInputPlus in, int version) throws IOException + { + ConsistencyLevel consistency = ConsistencyLevel.fromCode(in.readUnsignedByte()); + int received = in.readUnsignedVInt32(); + int blockFor = in.readUnsignedVInt32(); + boolean dataPresent = in.readBoolean(); + return new ReadTimeoutException(consistency, received, blockFor, dataPresent, message); + } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.READ_TIMEOUT; + } } diff --git a/src/java/org/apache/cassandra/exceptions/RequestFailureException.java b/src/java/org/apache/cassandra/exceptions/RequestFailureException.java index 2998032716..24e2bb8de8 100644 --- a/src/java/org/apache/cassandra/exceptions/RequestFailureException.java +++ b/src/java/org/apache/cassandra/exceptions/RequestFailureException.java @@ -23,7 +23,7 @@ import java.util.stream.Collectors; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.locator.InetAddressAndPort; -public class RequestFailureException extends RequestExecutionException +public abstract class RequestFailureException extends RequestExecutionException { public final ConsistencyLevel consistency; public final int received; diff --git a/src/java/org/apache/cassandra/exceptions/RequestTimeoutException.java b/src/java/org/apache/cassandra/exceptions/RequestTimeoutException.java index 992dfba791..3f04b68a12 100644 --- a/src/java/org/apache/cassandra/exceptions/RequestTimeoutException.java +++ b/src/java/org/apache/cassandra/exceptions/RequestTimeoutException.java @@ -19,7 +19,7 @@ package org.apache.cassandra.exceptions; import org.apache.cassandra.db.ConsistencyLevel; -public class RequestTimeoutException extends RequestExecutionException +public abstract class RequestTimeoutException extends RequestExecutionException { public final ConsistencyLevel consistency; public final int received; diff --git a/src/java/org/apache/cassandra/exceptions/SyntaxException.java b/src/java/org/apache/cassandra/exceptions/SyntaxException.java index 3a3db35c75..ae560f1022 100644 --- a/src/java/org/apache/cassandra/exceptions/SyntaxException.java +++ b/src/java/org/apache/cassandra/exceptions/SyntaxException.java @@ -23,4 +23,10 @@ public class SyntaxException extends RequestValidationException { super(ExceptionCode.SYNTAX_ERROR, msg); } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.SYNTAX_ERROR; + } } diff --git a/src/java/org/apache/cassandra/exceptions/TombstoneAbortException.java b/src/java/org/apache/cassandra/exceptions/TombstoneAbortException.java index cd0c98efac..4ba4705225 100644 --- a/src/java/org/apache/cassandra/exceptions/TombstoneAbortException.java +++ b/src/java/org/apache/cassandra/exceptions/TombstoneAbortException.java @@ -18,9 +18,13 @@ package org.apache.cassandra.exceptions; +import java.io.IOException; import java.util.Map; import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.locator.InetAddressAndPort; public class TombstoneAbortException extends ReadAbortException @@ -36,4 +40,38 @@ public class TombstoneAbortException extends ReadAbortException this.nodes = nodes; this.tombstones = tombstones; } + + @Override + protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException + { + // Serialize parent fields first + super.serializeSpecificFields(out, version); + // Add TombstoneAbortException specific fields + out.writeUnsignedVInt32(nodes); + out.writeUnsignedVInt(tombstones); + } + + @Override + protected long serializedSizeSpecificFields(int version) + { + return super.serializedSizeSpecificFields(version) + + TypeSizes.sizeofUnsignedVInt(nodes) + + TypeSizes.sizeofUnsignedVInt(tombstones); + } + + static TombstoneAbortException deserializeFields(String message, DataInputPlus in, int version) throws IOException + { + ReadFailureException.DeserializedFields fields = ReadFailureException.deserializeBaseFields(in, version); + int nodes = in.readUnsignedVInt32(); + long tombstones = in.readUnsignedVInt(); + + return new TombstoneAbortException(message, nodes, tombstones, fields.dataPresent, + fields.consistency, fields.received, fields.blockFor, fields.failures); + } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.TOMBSTONE_ABORT; + } } diff --git a/src/java/org/apache/cassandra/exceptions/TruncateException.java b/src/java/org/apache/cassandra/exceptions/TruncateException.java index 6d6e0272da..10c5dabf16 100644 --- a/src/java/org/apache/cassandra/exceptions/TruncateException.java +++ b/src/java/org/apache/cassandra/exceptions/TruncateException.java @@ -28,4 +28,10 @@ public class TruncateException extends RequestExecutionException { super(ExceptionCode.TRUNCATE_ERROR, msg); } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.TRUNCATE_ERROR; + } } diff --git a/src/java/org/apache/cassandra/exceptions/UnauthorizedException.java b/src/java/org/apache/cassandra/exceptions/UnauthorizedException.java index 008d793545..a78c6dbe63 100644 --- a/src/java/org/apache/cassandra/exceptions/UnauthorizedException.java +++ b/src/java/org/apache/cassandra/exceptions/UnauthorizedException.java @@ -28,4 +28,10 @@ public class UnauthorizedException extends RequestValidationException { super(ExceptionCode.UNAUTHORIZED, msg, e); } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.UNAUTHORIZED; + } } diff --git a/src/java/org/apache/cassandra/exceptions/UnavailableException.java b/src/java/org/apache/cassandra/exceptions/UnavailableException.java index 8454a68f9a..ccbe70acef 100644 --- a/src/java/org/apache/cassandra/exceptions/UnavailableException.java +++ b/src/java/org/apache/cassandra/exceptions/UnavailableException.java @@ -17,7 +17,12 @@ */ package org.apache.cassandra.exceptions; +import java.io.IOException; + import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; public class UnavailableException extends RequestExecutionException { @@ -55,4 +60,34 @@ public class UnavailableException extends RequestExecutionException this.required = required; this.alive = alive; } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.UNAVAILABLE; + } + + @Override + protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException + { + out.writeByte(consistency.code); + out.writeUnsignedVInt32(required); + out.writeUnsignedVInt32(alive); + } + + @Override + protected long serializedSizeSpecificFields(int version) + { + return TypeSizes.BYTE_SIZE + // consistency + TypeSizes.sizeofUnsignedVInt(required) + + TypeSizes.sizeofUnsignedVInt(alive); + } + + static UnavailableException deserializeFields(String message, DataInputPlus in, int version) throws IOException + { + ConsistencyLevel consistency = ConsistencyLevel.fromCode(in.readUnsignedByte()); + int required = in.readUnsignedVInt32(); + int alive = in.readUnsignedVInt32(); + return create(consistency, required, alive); + } } diff --git a/src/java/org/apache/cassandra/exceptions/WriteFailureException.java b/src/java/org/apache/cassandra/exceptions/WriteFailureException.java index bcda9e675f..3066ca57cc 100644 --- a/src/java/org/apache/cassandra/exceptions/WriteFailureException.java +++ b/src/java/org/apache/cassandra/exceptions/WriteFailureException.java @@ -17,13 +17,18 @@ */ package org.apache.cassandra.exceptions; +import java.io.IOException; import java.util.Map; import com.google.common.collect.ImmutableMap; import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.WriteType; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.utils.CollectionSerializers; public class WriteFailureException extends RequestFailureException { @@ -34,4 +39,56 @@ public class WriteFailureException extends RequestFailureException super(ExceptionCode.WRITE_FAILURE, consistency, received, blockFor, ImmutableMap.copyOf(failureReasonByEndpoint)); this.writeType = writeType; } + + @Override + protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException + { + out.writeByte(consistency.code); + out.writeUnsignedVInt32(received); + out.writeUnsignedVInt32(blockFor); + + // Serialize failure reason map + CollectionSerializers.serializeMap(failureReasonByEndpoint, out, version, + InetAddressAndPort.Serializer.inetAddressAndPortSerializer, + RequestFailureReason.serializer); + + WriteType.serializer.serialize(writeType, out); + } + + @Override + protected long serializedSizeSpecificFields(int version) + { + long size = TypeSizes.BYTE_SIZE + // consistency + TypeSizes.sizeofUnsignedVInt(received) + + TypeSizes.sizeofUnsignedVInt(blockFor); + + size += CollectionSerializers.serializedMapSize(failureReasonByEndpoint, version, + InetAddressAndPort.Serializer.inetAddressAndPortSerializer, + RequestFailureReason.serializer); + + size += WriteType.serializer.serializedSize(writeType); + return size; + } + + static WriteFailureException deserializeFields(String message, DataInputPlus in, int version) throws IOException + { + ConsistencyLevel consistency = ConsistencyLevel.fromCode(in.readUnsignedByte()); + int received = in.readUnsignedVInt32(); + int blockFor = in.readUnsignedVInt32(); + + // Deserialize failure reason map + Map failures = + CollectionSerializers.deserializeMap(in, version, + InetAddressAndPort.Serializer.inetAddressAndPortSerializer, + RequestFailureReason.serializer); + + WriteType writeType = WriteType.serializer.deserialize(in); + return new WriteFailureException(consistency, received, blockFor, writeType, failures); + } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.WRITE_FAILURE; + } } diff --git a/src/java/org/apache/cassandra/exceptions/WriteTimeoutException.java b/src/java/org/apache/cassandra/exceptions/WriteTimeoutException.java index 33c96c951e..f516a707cb 100644 --- a/src/java/org/apache/cassandra/exceptions/WriteTimeoutException.java +++ b/src/java/org/apache/cassandra/exceptions/WriteTimeoutException.java @@ -17,8 +17,13 @@ */ package org.apache.cassandra.exceptions; +import java.io.IOException; + import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.WriteType; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; public class WriteTimeoutException extends RequestTimeoutException { @@ -36,4 +41,37 @@ public class WriteTimeoutException extends RequestTimeoutException super(ExceptionCode.WRITE_TIMEOUT, consistency, received, blockFor, msg); this.writeType = writeType; } + + @Override + protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException + { + out.write(consistency.code); + out.writeUnsignedVInt32(received); + out.writeUnsignedVInt32(blockFor); + WriteType.serializer.serialize(writeType, out); + } + + @Override + protected long serializedSizeSpecificFields(int version) + { + return TypeSizes.BYTE_SIZE + // consistency + TypeSizes.sizeofUnsignedVInt(received) + + TypeSizes.sizeofUnsignedVInt(blockFor) + + WriteType.serializer.serializedSize(writeType); + } + + static WriteTimeoutException deserializeFields(String message, DataInputPlus in, int version) throws IOException + { + ConsistencyLevel consistency = ConsistencyLevel.fromCode(in.readUnsignedByte()); + int received = in.readUnsignedVInt32(); + int blockFor = in.readUnsignedVInt32(); + WriteType writeType = WriteType.serializer.deserialize(in); + return new WriteTimeoutException(writeType, consistency, received, blockFor, message); + } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.WRITE_TIMEOUT; + } } diff --git a/src/java/org/apache/cassandra/io/AsymmetricUnversionedSerializer.java b/src/java/org/apache/cassandra/io/AsymmetricUnversionedSerializer.java index 34b0a185f8..355e87777e 100644 --- a/src/java/org/apache/cassandra/io/AsymmetricUnversionedSerializer.java +++ b/src/java/org/apache/cassandra/io/AsymmetricUnversionedSerializer.java @@ -26,7 +26,11 @@ import org.apache.cassandra.io.util.DataInputBuffer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputBuffer; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.utils.Shared; +import static org.apache.cassandra.utils.Shared.Scope.SIMULATION; + +@Shared(scope = SIMULATION) public interface AsymmetricUnversionedSerializer { void serialize(In t, DataOutputPlus out) throws IOException; diff --git a/src/java/org/apache/cassandra/io/IVersionedAsymmetricSerializer.java b/src/java/org/apache/cassandra/io/IVersionedAsymmetricSerializer.java index 06469fa129..9ba93122a2 100644 --- a/src/java/org/apache/cassandra/io/IVersionedAsymmetricSerializer.java +++ b/src/java/org/apache/cassandra/io/IVersionedAsymmetricSerializer.java @@ -25,7 +25,11 @@ import org.apache.cassandra.io.util.DataInputBuffer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputBuffer; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.utils.Shared; +import static org.apache.cassandra.utils.Shared.Scope.SIMULATION; + +@Shared(scope = SIMULATION) public interface IVersionedAsymmetricSerializer { /** diff --git a/src/java/org/apache/cassandra/io/IVersionedSerializer.java b/src/java/org/apache/cassandra/io/IVersionedSerializer.java index 0e5a400ff1..2fa61d3c9b 100644 --- a/src/java/org/apache/cassandra/io/IVersionedSerializer.java +++ b/src/java/org/apache/cassandra/io/IVersionedSerializer.java @@ -21,7 +21,11 @@ import java.io.IOException; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.utils.Shared; +import static org.apache.cassandra.utils.Shared.Scope.SIMULATION; + +@Shared(scope = SIMULATION) public interface IVersionedSerializer extends IVersionedAsymmetricSerializer { static IVersionedSerializer from(UnversionedSerializer delegate) diff --git a/src/java/org/apache/cassandra/io/UnversionedSerializer.java b/src/java/org/apache/cassandra/io/UnversionedSerializer.java index 2bf1f2013a..6c78b4ac3b 100644 --- a/src/java/org/apache/cassandra/io/UnversionedSerializer.java +++ b/src/java/org/apache/cassandra/io/UnversionedSerializer.java @@ -18,6 +18,11 @@ package org.apache.cassandra.io; +import org.apache.cassandra.utils.Shared; + +import static org.apache.cassandra.utils.Shared.Scope.SIMULATION; + +@Shared(scope = SIMULATION) public interface UnversionedSerializer extends AsymmetricUnversionedSerializer { } diff --git a/src/java/org/apache/cassandra/locator/EndpointsForToken.java b/src/java/org/apache/cassandra/locator/EndpointsForToken.java index fc88fa761b..9af475b0ed 100644 --- a/src/java/org/apache/cassandra/locator/EndpointsForToken.java +++ b/src/java/org/apache/cassandra/locator/EndpointsForToken.java @@ -18,13 +18,19 @@ package org.apache.cassandra.locator; +import java.io.IOException; import java.util.Arrays; import java.util.Collection; import com.google.common.base.Preconditions; import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.IPartitionerDependentSerializer; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.tcm.ownership.VersionedEndpoints; @@ -167,4 +173,38 @@ public class EndpointsForToken extends Endpoints return ClusterMetadata.current().placements.get(keyspace.getMetadata().params.replication).reads.forToken(token); } + public static final IPartitionerDependentSerializer serializer = new Serializer(); + + public static class Serializer implements IPartitionerDependentSerializer + { + @Override + public void serialize(EndpointsForToken endpoints, DataOutputPlus out, int version) throws IOException + { + Token.compactSerializer.serialize(endpoints.token(), out, version); + out.writeUnsignedVInt32(endpoints.size()); + for (Replica replica : endpoints) + Replica.serializer.serialize(replica, out, version); + } + + @Override + public EndpointsForToken deserialize(DataInputPlus in, IPartitioner partitioner, int version) throws IOException + { + Token token = Token.compactSerializer.deserialize(in, partitioner, version); + int size = in.readUnsignedVInt32(); + EndpointsForToken.Builder builder = EndpointsForToken.builder(token, size); + for (int i = 0; i < size; i++) + builder.add(Replica.serializer.deserialize(in, partitioner, version)); + return builder.build(); + } + + @Override + public long serializedSize(EndpointsForToken endpoints, int version) + { + long size = Token.compactSerializer.serializedSize(endpoints.token(), version); + size += TypeSizes.sizeofUnsignedVInt(endpoints.size()); + for (Replica replica : endpoints) + size += Replica.serializer.serializedSize(replica, version); + return size; + } + } } diff --git a/src/java/org/apache/cassandra/locator/ReplicaPlans.java b/src/java/org/apache/cassandra/locator/ReplicaPlans.java index 6ae2aeadb4..01de6d4a5d 100644 --- a/src/java/org/apache/cassandra/locator/ReplicaPlans.java +++ b/src/java/org/apache/cassandra/locator/ReplicaPlans.java @@ -754,8 +754,6 @@ public class ReplicaPlans ReplicaLayout.ForTokenWrite liveAndDown = ReplicaLayout.forTokenWriteLiveAndDown(metadata, keyspace, tk); - Replicas.temporaryAssertFull(liveAndDown.all()); // TODO CASSANDRA-14547 - if (consistencyForPaxos == ConsistencyLevel.LOCAL_SERIAL) { // TODO: we should cleanup our semantics here, as we're filtering ALL nodes to localDC which is unexpected for ReplicaPlan diff --git a/src/java/org/apache/cassandra/net/Message.java b/src/java/org/apache/cassandra/net/Message.java index f8ecfd58a6..b68de12180 100644 --- a/src/java/org/apache/cassandra/net/Message.java +++ b/src/java/org/apache/cassandra/net/Message.java @@ -237,12 +237,27 @@ public class Message implements ResponseContext return out(verb, payload); } + public static Message out(Verb verb, T payload, boolean isUrgent, long expiresAtNanos) + { + assert !verb.isResponse(); + if (isUrgent) + return outWithFlag(verb, payload, MessageFlag.URGENT, expiresAtNanos); + else + return out(verb, payload, expiresAtNanos); + } + public static Message outWithFlag(Verb verb, T payload, MessageFlag flag) { assert !verb.isResponse(); return outWithParam(nextId(), verb, 0, payload, flag.addTo(0), null, null); } + public static Message outWithFlag(Verb verb, T payload, MessageFlag flag, long expiresAtNanos) + { + assert !verb.isResponse(); + return outWithParam(nextId(), verb, expiresAtNanos, payload, flag.addTo(0), null, null); + } + public static Message outWithParam(Verb verb, T payload, ParamType paramType, Object paramValue) { assert !verb.isResponse() : verb; diff --git a/src/java/org/apache/cassandra/net/Verb.java b/src/java/org/apache/cassandra/net/Verb.java index d884ad1a2a..3abc4beddb 100644 --- a/src/java/org/apache/cassandra/net/Verb.java +++ b/src/java/org/apache/cassandra/net/Verb.java @@ -121,10 +121,22 @@ import org.apache.cassandra.service.accord.serializers.SetDurableSerializers; import org.apache.cassandra.service.accord.serializers.Version; import org.apache.cassandra.service.consensus.migration.ConsensusKeyMigrationState; import org.apache.cassandra.service.consensus.migration.ConsensusKeyMigrationState.ConsensusKeyMigrationFinished; +import org.apache.cassandra.service.paxos.CasForwardHandler; +import org.apache.cassandra.service.paxos.CasForwardRequest; +import org.apache.cassandra.service.paxos.CasForwardResponse; import org.apache.cassandra.service.paxos.Commit; import org.apache.cassandra.service.paxos.Commit.Agreed; +import org.apache.cassandra.service.paxos.ConsensusReadForwardHandler; +import org.apache.cassandra.service.paxos.ConsensusReadForwardRequest; +import org.apache.cassandra.service.paxos.Paxos2CommitForwardHandler; +import org.apache.cassandra.service.paxos.Paxos2CommitForwardRequest; import org.apache.cassandra.service.paxos.PaxosCommit; import org.apache.cassandra.service.paxos.PaxosCommitAndPrepare; +import org.apache.cassandra.service.paxos.PaxosCommitForwardHandler; +import org.apache.cassandra.service.paxos.PaxosCommitForwardRequest; +import org.apache.cassandra.service.paxos.PrepareRefreshForwardHandler; +import org.apache.cassandra.service.paxos.PrepareRefreshForwardRequest; +import org.apache.cassandra.service.paxos.PrepareRefreshForwardResponse; import org.apache.cassandra.service.paxos.PaxosPrepare; import org.apache.cassandra.service.paxos.PaxosPrepareRefresh; import org.apache.cassandra.service.paxos.PaxosPropose; @@ -174,6 +186,7 @@ import static org.apache.cassandra.concurrent.Stage.MUTATION; import static org.apache.cassandra.concurrent.Stage.PAXOS_REPAIR; import static org.apache.cassandra.concurrent.Stage.READ; import static org.apache.cassandra.concurrent.Stage.REQUEST_RESPONSE; +import static org.apache.cassandra.concurrent.Stage.TRACKED_CAS; import static org.apache.cassandra.concurrent.Stage.TRACING; import static org.apache.cassandra.net.ResponseHandlerSupplier.RESPONSE_HANDLER; import static org.apache.cassandra.net.Verb.Kind.CUSTOM; @@ -227,6 +240,8 @@ public enum Verb PAXOS_PROPOSE_REQ (34, P2, writeTimeout, MUTATION, () -> Commit.serializer, () -> ProposeVerbHandler.instance, PAXOS_PROPOSE_RSP ), PAXOS_COMMIT_RSP (95, P2, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, RESPONSE_HANDLER ), PAXOS_COMMIT_REQ (35, P2, writeTimeout, MUTATION, () -> Agreed.serializer, () -> PaxosCommit.requestHandler, PAXOS_COMMIT_RSP ), + PAXOS_COMMIT_FORWARD_RSP (96, P2, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, RESPONSE_HANDLER ), + PAXOS_COMMIT_FORWARD_REQ (32, P2, writeTimeout, TRACKED_CAS, () -> PaxosCommitForwardRequest.serializer, () -> PaxosCommitForwardHandler.instance, PAXOS_COMMIT_FORWARD_RSP ), TRUNCATE_RSP (79, P0, truncateTimeout, REQUEST_RESPONSE, () -> TruncateResponse.serializer, RESPONSE_HANDLER ), TRUNCATE_REQ (19, P0, truncateTimeout, MUTATION, () -> TruncateRequest.serializer, () -> TruncateVerbHandler.instance, TRUNCATE_RSP ), @@ -308,6 +323,16 @@ public enum Verb PAXOS2_CLEANUP_COMPLETE_REQ (48, P2, repairTimeout, PAXOS_REPAIR, () -> PaxosCleanupComplete.serializer, () -> PaxosCleanupComplete.verbHandler, PAXOS2_CLEANUP_COMPLETE_RSP ), PAXOS2_UPDATE_LOW_BALLOT_RSP (67, P2, repairTimeout, PAXOS_REPAIR, () -> NoPayload.serializer, RESPONSE_HANDLER ), PAXOS2_UPDATE_LOW_BALLOT_REQ (64, P2, repairTimeout, PAXOS_REPAIR, () -> PaxosUpdateLowBallot.serializer, () -> PaxosUpdateLowBallot.verbHandler, PAXOS2_UPDATE_LOW_BALLOT_RSP ), + PAXOS2_COMMIT_FORWARD_RSP (71, P2, writeTimeout, REQUEST_RESPONSE, () -> NoPayload.serializer, RESPONSE_HANDLER ), + PAXOS2_COMMIT_FORWARD_REQ (72, P2, writeTimeout, TRACKED_CAS, () -> Paxos2CommitForwardRequest.serializer, () -> Paxos2CommitForwardHandler.instance, PAXOS2_COMMIT_FORWARD_RSP ), + + // CAS and consensus read forwarding for tracked keyspaces + CAS_FORWARD_RSP (73, P2, writeTimeout, REQUEST_RESPONSE, () -> CasForwardResponse.serializer, RESPONSE_HANDLER ), + CAS_FORWARD_REQ (74, P2, writeTimeout, TRACKED_CAS, () -> CasForwardRequest.serializer, () -> CasForwardHandler.instance, CAS_FORWARD_RSP ), + CONSENSUS_READ_FORWARD_RSP (75, P2, readTimeout, REQUEST_RESPONSE, () -> CasForwardResponse.serializer, RESPONSE_HANDLER ), + CONSENSUS_READ_FORWARD_REQ (76, P2, readTimeout, TRACKED_CAS, () -> ConsensusReadForwardRequest.serializer, () -> ConsensusReadForwardHandler.instance, CONSENSUS_READ_FORWARD_RSP ), + PAXOS_PREPARE_REFRESH_FORWARD_RSP(77, P2, writeTimeout, REQUEST_RESPONSE, () -> PrepareRefreshForwardResponse.serializer,RESPONSE_HANDLER ), + PAXOS_PREPARE_REFRESH_FORWARD_REQ(78, P2, writeTimeout, TRACKED_CAS, () -> PrepareRefreshForwardRequest.serializer, () -> PrepareRefreshForwardHandler.instance, PAXOS_PREPARE_REFRESH_FORWARD_RSP), // transactional cluster metadata TCM_COMMIT_RSP (801, P0, rpcTimeout, INTERNAL_METADATA, MessageSerializers::commitResultSerializer, RESPONSE_HANDLER ), diff --git a/src/java/org/apache/cassandra/replication/MutationId.java b/src/java/org/apache/cassandra/replication/MutationId.java index 8667945a04..16c3ba42d8 100644 --- a/src/java/org/apache/cassandra/replication/MutationId.java +++ b/src/java/org/apache/cassandra/replication/MutationId.java @@ -18,6 +18,7 @@ package org.apache.cassandra.replication; import java.io.IOException; +import java.nio.ByteBuffer; import java.util.Comparator; import org.apache.cassandra.db.TypeSizes; @@ -113,6 +114,29 @@ public class MutationId extends ShortMutationId */ public static final Comparator comparator = ShortMutationId.comparator::compare; + public ByteBuffer toByteBuffer() + { + return ByteBuffer.allocate(16) + .putLong(logId()) + .putLong(sequenceId()) + .flip(); + } + + public static MutationId fromByteBuffer(ByteBuffer buffer) + { + if (buffer.remaining() < 16) + throw new IllegalStateException(); + + int pos = buffer.position(); + long logId = buffer.getLong(pos); + long sequenceId = buffer.getLong(pos + 8); + + if (logId == MutationId.none().logId() && sequenceId == MutationId.none().sequenceId()) + return MutationId.none(); + + return new MutationId(logId, sequenceId); + } + public static class Serializer implements IVersionedSerializer { @Override diff --git a/src/java/org/apache/cassandra/replication/TrackedWriteRequest.java b/src/java/org/apache/cassandra/replication/TrackedWriteRequest.java index e47b8766e6..ba35f231d9 100644 --- a/src/java/org/apache/cassandra/replication/TrackedWriteRequest.java +++ b/src/java/org/apache/cassandra/replication/TrackedWriteRequest.java @@ -398,6 +398,9 @@ public class TrackedWriteRequest static void applyMutationLocally(Mutation mutation, RequestCallback handler) { Preconditions.checkArgument(handler instanceof TrackedWriteResponseHandler || handler instanceof ForwardedWrite.LeaderCallback); + // TODO: Why execute immediately before even sending the messages to the other nodes? + // Also this is already going to be on the mutation stage and the mutation stage can get backed up so don't we want + // to start on a stage that is less likley to get backed up? Stage.MUTATION.maybeExecuteImmediately(new LocalMutationRunnable(mutation, handler)); } diff --git a/src/java/org/apache/cassandra/service/CASRequest.java b/src/java/org/apache/cassandra/service/CASRequest.java deleted file mode 100644 index 37bfdcec70..0000000000 --- a/src/java/org/apache/cassandra/service/CASRequest.java +++ /dev/null @@ -1,61 +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; - -import accord.primitives.Txn; - -import org.apache.cassandra.db.ConsistencyLevel; -import org.apache.cassandra.db.SinglePartitionReadCommand; -import org.apache.cassandra.db.partitions.FilteredPartition; -import org.apache.cassandra.db.partitions.PartitionUpdate; -import org.apache.cassandra.exceptions.InvalidRequestException; -import org.apache.cassandra.service.accord.txn.TxnResult; -import org.apache.cassandra.service.paxos.Ballot; -import org.apache.cassandra.tcm.ClusterMetadata; -import org.apache.cassandra.transport.Dispatcher; - -import static org.apache.cassandra.service.StorageProxy.ConsensusAttemptResult; - -/** - * Abstract the conditions and updates for a CAS operation. - */ -public interface CASRequest -{ - Dispatcher.RequestTime requestTime(); - - /** - * The command to use to fetch the value to compare for the CAS. - */ - SinglePartitionReadCommand readCommand(long nowInSec); - - /** - * Returns whether the provided CF, that represents the values fetched using the - * readFilter(), match the CAS conditions this object stands for. - */ - boolean appliesTo(FilteredPartition current) throws InvalidRequestException; - - /** - * The updates to perform of a CAS success. The values fetched using the readFilter() - * are passed as argument. - */ - PartitionUpdate makeUpdates(FilteredPartition current, ClientState clientState, Ballot ballot) throws InvalidRequestException; - - Txn toAccordTxn(ClusterMetadata cm, ConsistencyLevel consistencyLevel, ConsistencyLevel commitConsistencyLevel, ClientState clientState, long nowInSecs); - - ConsensusAttemptResult toCasResult(TxnResult txnResult); -} diff --git a/src/java/org/apache/cassandra/service/ClientState.java b/src/java/org/apache/cassandra/service/ClientState.java index 7ec2160e8d..cee123bca1 100644 --- a/src/java/org/apache/cassandra/service/ClientState.java +++ b/src/java/org/apache/cassandra/service/ClientState.java @@ -195,6 +195,13 @@ public class ClientState this.remoteAddress = null; } + private ClientState(boolean isInternal, boolean applyGuardrails) + { + this.isInternal = isInternal; + this.applyGuardrails = applyGuardrails; + this.remoteAddress = null; + } + protected ClientState(InetSocketAddress remoteAddress) { this.isInternal = false; @@ -237,6 +244,21 @@ public class ClientState return new ClientState((InetSocketAddress)remoteAddress); } + /** + * @return a minimal ClientState for forwarded tracked Paxos requests (for guardrail application) + */ + public static ClientState forForwardedCalls(boolean isInternal, boolean applyGuardrails, boolean isSuper) + { + return new ClientState(isInternal, applyGuardrails) + { + @Override + public boolean isSuper() + { + return isSuper; + } + }; + } + /** * Clone this ClientState object, but use the provided keyspace instead of the * keyspace in this ClientState object. diff --git a/src/java/org/apache/cassandra/service/StorageProxy.java b/src/java/org/apache/cassandra/service/StorageProxy.java index 43d58157ac..29fcb17c93 100644 --- a/src/java/org/apache/cassandra/service/StorageProxy.java +++ b/src/java/org/apache/cassandra/service/StorageProxy.java @@ -57,6 +57,7 @@ import org.slf4j.LoggerFactory; import accord.primitives.Txn; +import org.agrona.collections.IntHashSet; import org.apache.cassandra.batchlog.Batch; import org.apache.cassandra.batchlog.BatchlogManager; import org.apache.cassandra.concurrent.DebuggableTask.RunnableDebuggableTask; @@ -65,6 +66,7 @@ import org.apache.cassandra.config.AccordSpec; import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.Config; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.statements.CQL3CasRequest; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.CounterMutation; @@ -72,6 +74,7 @@ import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.EmptyIterators; import org.apache.cassandra.db.IMutation; import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.KeyspaceNotDefinedException; import org.apache.cassandra.db.MessageParams; import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.PartitionPosition; @@ -96,6 +99,7 @@ import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.CasWriteTimeoutException; import org.apache.cassandra.exceptions.CasWriteUnknownResultException; +import org.apache.cassandra.exceptions.CassandraException; import org.apache.cassandra.exceptions.CoordinatorBehindException; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.exceptions.IsBootstrappingException; @@ -112,6 +116,7 @@ import org.apache.cassandra.exceptions.UnavailableException; import org.apache.cassandra.exceptions.WriteFailureException; import org.apache.cassandra.exceptions.WriteTimeoutException; import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.gms.FailureDetector; import org.apache.cassandra.hints.Hint; import org.apache.cassandra.hints.HintsService; import org.apache.cassandra.locator.AbstractReplicationStrategy; @@ -132,9 +137,13 @@ import org.apache.cassandra.net.ForwardingInfo; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessageFlag; import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.NoPayload; import org.apache.cassandra.net.RequestCallback; import org.apache.cassandra.net.Verb; +import org.apache.cassandra.replication.MutationId; +import org.apache.cassandra.replication.MutationTrackingService; import org.apache.cassandra.replication.TrackedWriteRequest; +import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.PartitionDenylist; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.SchemaConstants; @@ -164,9 +173,13 @@ import org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter.C 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.CasForwardRequest; +import org.apache.cassandra.service.paxos.CasForwardResponse; import org.apache.cassandra.service.paxos.Commit; +import org.apache.cassandra.service.paxos.ConsensusReadForwardRequest; import org.apache.cassandra.service.paxos.ContentionStrategy; import org.apache.cassandra.service.paxos.Paxos; +import org.apache.cassandra.service.paxos.PaxosCommitForwardRequest; import org.apache.cassandra.service.paxos.PaxosState; import org.apache.cassandra.service.paxos.v1.PrepareCallback; import org.apache.cassandra.service.paxos.v1.ProposeCallback; @@ -189,13 +202,17 @@ import org.apache.cassandra.utils.NoSpamLogger; import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.Throwables; import org.apache.cassandra.utils.TimeUUID; +import org.apache.cassandra.utils.concurrent.AsyncPromise; import org.apache.cassandra.utils.concurrent.CountDownLatch; import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.concurrent.Promise; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; import static accord.primitives.Txn.Kind.Read; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.Iterables.concat; +import static java.util.Collections.singleton; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.NANOSECONDS; import static org.apache.cassandra.db.ConsistencyLevel.SERIAL; @@ -210,8 +227,10 @@ import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.writeMetr import static org.apache.cassandra.net.Message.out; import static org.apache.cassandra.net.NoPayload.noPayload; import static org.apache.cassandra.net.Verb.BATCH_STORE_REQ; +import static org.apache.cassandra.net.Verb.CONSENSUS_READ_FORWARD_REQ; import static org.apache.cassandra.net.Verb.MUTATION_REQ; import static org.apache.cassandra.net.Verb.PAXOS_COMMIT_REQ; +import static org.apache.cassandra.net.Verb.PAXOS_COMMIT_FORWARD_REQ; import static org.apache.cassandra.net.Verb.PAXOS_PREPARE_REQ; import static org.apache.cassandra.net.Verb.PAXOS_PROPOSE_REQ; import static org.apache.cassandra.net.Verb.SCHEMA_VERSION_REQ; @@ -250,6 +269,7 @@ public class StorageProxy implements StorageProxyMBean public static final String UNREACHABLE = "UNREACHABLE"; private static final int FAILURE_LOGGING_INTERVAL_SECONDS = CassandraRelevantProperties.FAILURE_LOGGING_INTERVAL_SECONDS.getInt(); + private static final boolean DISABLE_CONSENSUS_REQUEST_FORWARDING = CassandraRelevantProperties.DISABLE_CONSENSUS_REQUEST_FORWARDING.getBoolean(); private static final String UNSAFE_MIXED_MUTATIONS_MSG = "Mutations look to have different time sources, some are using 'USING TIMESTAMP' and others are using the server timestamp; writes to the Accord table will not be linearizable while using transactions. To allow this behavior set accord.mixed_time_source_handling=log or ignore"; private static final WritePerformer standardWritePerformer; @@ -367,13 +387,48 @@ public class StorageProxy implements StorageProxyMBean public static RowIterator cas(String keyspaceName, String cfName, DecoratedKey key, - CASRequest request, + CQL3CasRequest request, ConsistencyLevel consistencyForPaxos, ConsistencyLevel consistencyForCommit, ClientState clientState, long nowInSeconds, Dispatcher.RequestTime requestTime) throws UnavailableException, IsBootstrappingException, RequestFailureException, RequestTimeoutException, InvalidRequestException, CasWriteUnknownResultException + { + return casInternal(keyspaceName, cfName, key, request, consistencyForPaxos, consistencyForCommit, + clientState, nowInSeconds, requestTime, false); + } + + /** + * Version of cas called by handlers that have already received a forwarded request. + * This prevents infinite forwarding loops if the forwarding target is not actually a replica. + */ + public static RowIterator casForwarded(String keyspaceName, + String cfName, + DecoratedKey key, + CQL3CasRequest request, + ConsistencyLevel consistencyForPaxos, + ConsistencyLevel consistencyForCommit, + ClientState clientState, + long nowInSeconds, + Dispatcher.RequestTime requestTime) + throws UnavailableException, IsBootstrappingException, RequestFailureException, RequestTimeoutException, InvalidRequestException, CasWriteUnknownResultException + { + return casInternal(keyspaceName, cfName, key, request, consistencyForPaxos, consistencyForCommit, + clientState, nowInSeconds, requestTime, true); + } + + private static RowIterator casInternal(String keyspaceName, + String cfName, + DecoratedKey key, + CQL3CasRequest request, + ConsistencyLevel consistencyForPaxos, + ConsistencyLevel consistencyForCommit, + ClientState clientState, + long nowInSeconds, + Dispatcher.RequestTime requestTime, + boolean alreadyForwarded) + throws UnavailableException, IsBootstrappingException, RequestFailureException, RequestTimeoutException, InvalidRequestException, CasWriteUnknownResultException { if (DatabaseDescriptor.getPartitionDenylistEnabled() && DatabaseDescriptor.getDenylistWritesEnabled() && !partitionDenylist.isKeyPermitted(keyspaceName, cfName, key.getKey())) { @@ -382,6 +437,13 @@ public class StorageProxy implements StorageProxyMBean key, keyspaceName, cfName)); } + // Check if this CAS operation needs to be forwarded to a replica coordinator for tracked keyspaces + RowIterator forwardResult = checkAndForwardCasIfNeeded(keyspaceName, cfName, key, request, + consistencyForPaxos, consistencyForCommit, + clientState, nowInSeconds, requestTime, alreadyForwarded); + if (forwardResult != null) + return forwardResult; + ConsensusAttemptResult lastAttemptResult = null; do { @@ -432,7 +494,7 @@ public class StorageProxy implements StorageProxyMBean private static ConsensusAttemptResult legacyCas(TableMetadata metadata, DecoratedKey key, - CASRequest request, + CQL3CasRequest request, ConsistencyLevel consistencyForPaxos, ConsistencyLevel consistencyForCommit, ClientState clientState, @@ -628,7 +690,7 @@ public class StorageProxy implements StorageProxyMBean // because we also skip replaying those same empty update in beginAndRepairPaxos (see the longer // comment there). As empty update are somewhat common (serial reads and non-applying CAS propose // them), this is worth bothering. - if (!proposal.update.isEmpty()) + if (!proposal.isEmpty()) commitPaxos(proposal, consistencyForCommit, true, requestTime); RowIterator result = proposalPair.right; if (result != null) @@ -733,7 +795,7 @@ public class StorageProxy implements StorageProxyMBean // replayed in that case. // Tl;dr, it is safe to skip committing empty updates _as long as_ we also skip replying them below. And // doing is more efficient, so we do so. - if (!inProgress.update.isEmpty() && inProgress.isAfter(mostRecent)) + if (!inProgress.isEmpty() && inProgress.isAfter(mostRecent)) { Tracing.trace("Finishing incomplete paxos round {}", inProgress); casMetrics.unfinishedCommit.inc(); @@ -760,7 +822,7 @@ public class StorageProxy implements StorageProxyMBean if (Iterables.size(missingMRC) > 0) { Tracing.trace("Repairing replicas that missed the most recent commit"); - sendCommit(mostRecent, missingMRC); + sendCommit(mostRecent, missingMRC, paxosPlan); // TODO: provided commits don't invalid the prepare we just did above (which they don't), we could just wait // for all the missingMRC to acknowledge this commit and then move on with proposing our value. But that means // adding the ability to have commitPaxos block, which is exactly CASSANDRA-5442 will do. So once we have that @@ -781,19 +843,108 @@ public class StorageProxy implements StorageProxyMBean } /** - * Unlike commitPaxos, this does not wait for replies + * Unlike commitPaxos, this does not wait for replies. + * For tracked keyspaces, ensures proper mutation ID generation and tracking. */ - private static void sendCommit(Commit commit, Iterable replicas) + private static void sendCommit(Commit commit, Iterable targetReplicas, ReplicaPlan.ForPaxosWrite replicaPlan) { - Message message = Message.out(PAXOS_COMMIT_REQ, commit); - for (InetAddressAndPort target : replicas) - MessagingService.instance().send(message, target); + String ksName = commit.metadata().keyspace; + KeyspaceMetadata ksMetadata = Schema.instance.getKeyspaceMetadata(ksName); + + // Non-tracked keyspaces OR already has mutation ID: fire and forget to target replicas + if (ksMetadata == null || !ksMetadata.params.replicationType.isTracked() || !commit.mutation.id().isNone()) + { + Message message = Message.out(PAXOS_COMMIT_REQ, commit); + for (InetAddressAndPort target : targetReplicas) + MessagingService.instance().send(message, target); + return; + } + + // No mutation ID - need to generate one + InetAddressAndPort localEndpoint = FBUtilities.getBroadcastAddressAndPort(); + boolean localIsReplica = replicaPlan.liveAndDown().endpoints().contains(localEndpoint); + + if (localIsReplica) + { + // Local node is a replica - use commitPaxosTracked (generates ID, sends to ALL replicas) + Keyspace keyspace = Keyspace.open(ksName); + commitPaxosTracked(keyspace, commit, replicaPlan.consistencyLevel(), + Dispatcher.RequestTime.forImmediateExecution(), true); + } + else + { + // Local node is not a replica - forward to a replica coordinator + forwardPaxosCommit(commit, replicaPlan); + } + } + + /** + * Forwards a Paxos commit to a replica coordinator for tracked keyspaces. + * The coordinator will generate mutation ID and send to ALL replicas. + * This method waits for the coordinator to confirm it has sent the commits. + */ + private static void forwardPaxosCommit(Commit commit, ReplicaPlan.ForPaxosWrite replicaPlan) + { + InetAddressAndPort localEndpoint = FBUtilities.getBroadcastAddressAndPort(); + + // Get live replicas (excluding local node) to find a coordinator + EndpointsForToken liveReplicas = replicaPlan.live().filter(replica -> !replica.endpoint().equals(localEndpoint)); + + if (liveReplicas.isEmpty()) + { + logger.warn("No live replicas available to forward Paxos commit for tracked keyspace"); + return; + } + + // Sort by proximity and select the closest as coordinator + EndpointsForToken sortedReplicas = DatabaseDescriptor.getNodeProximity().sortedByProximity(localEndpoint, liveReplicas); + InetAddressAndPort replicaCoordinator = sortedReplicas.get(0).endpoint(); + + Tracing.trace("Forwarding Paxos commit to replica coordinator {}", replicaCoordinator); + + // Use respondAfterSend=true so coordinator responds after sending commits (not waiting for application) + PaxosCommitForwardRequest forwardRequest = new PaxosCommitForwardRequest(commit, replicaPlan.consistencyLevel(), true); + Message message = Message.out(PAXOS_COMMIT_FORWARD_REQ, forwardRequest); + + // Wait for coordinator to confirm commits were sent before returning + Promise promise = new AsyncPromise<>(); + + RequestCallback callback = new RequestCallback() + { + @Override + public void onResponse(Message response) + { + promise.setSuccess(response.payload); + } + + @Override + public void onFailure(InetAddressAndPort from, RequestFailure reason) + { + promise.setFailure(new RuntimeException("Failed to forward Paxos commit to " + from + ": " + reason)); + } + }; + + MessagingService.instance().sendWithCallback(message, replicaCoordinator, callback); + + try + { + // Wait for coordinator to confirm commits were sent + promise.get(DatabaseDescriptor.getWriteRpcTimeout(MILLISECONDS), MILLISECONDS); + } + catch (TimeoutException e) + { + logger.warn("Timeout waiting for forwarded Paxos commit response from {}", replicaCoordinator); + } + catch (Exception e) + { + logger.warn("Error waiting for forwarded Paxos commit response from {}", replicaCoordinator, e); + } } private static PrepareCallback preparePaxos(Commit toPrepare, ReplicaPlan.ForPaxosWrite replicaPlan, Dispatcher.RequestTime requestTime) throws WriteTimeoutException { - PrepareCallback callback = new PrepareCallback(toPrepare.update.partitionKey(), toPrepare.update.metadata(), replicaPlan.requiredParticipants(), replicaPlan.consistencyLevel(), requestTime); + PrepareCallback callback = new PrepareCallback(toPrepare.partitionKey(), toPrepare.metadata(), replicaPlan.requiredParticipants(), replicaPlan.consistencyLevel(), requestTime); Message message = Message.out(PAXOS_PREPARE_REQ, toPrepare); boolean hasLocalRequest = false; @@ -873,10 +1024,139 @@ public class StorageProxy implements StorageProxyMBean private static void commitPaxos(Commit proposal, ConsistencyLevel consistencyLevel, boolean allowHints, Dispatcher.RequestTime requestTime) throws WriteTimeoutException { - boolean shouldBlock = consistencyLevel != ConsistencyLevel.ANY; - Keyspace keyspace = Keyspace.open(proposal.update.metadata().keyspace); + checkArgument(!proposal.isEmpty()); + // Check if this is a tracked keyspace + String keyspaceName = proposal.metadata().keyspace; + Keyspace keyspace = Keyspace.openIfExists(keyspaceName); + if (keyspace == null) + throw new KeyspaceNotDefinedException("Keyspace " + keyspaceName + " does not exist"); + KeyspaceMetadata ksMetadata = keyspace.getMetadata(); + + if (ksMetadata.params.replicationType.isTracked()) + { + // For tracked keyspaces, check if we need to forward or execute directly + Token tk = proposal.partitionKey().getToken(); + ReplicaPlan.ForWrite replicaPlan = ReplicaPlans.forWrite(keyspace, consistencyLevel, tk, ReplicaPlans.writeAll); + + if (isTrackedKeyspaceRequiringPaxosCommitForwarding(ksMetadata, proposal, replicaPlan.liveAndDown())) + { + // Forward to a replica coordinator + forwardPaxosCommit(proposal, consistencyLevel, replicaPlan); + } + else + { + // Execute directly using tracked logic + commitPaxosTracked(keyspace, proposal, consistencyLevel, requestTime); + } + } + else + { + // For untracked keyspaces, use existing logic + commitPaxosUntracked(keyspace, proposal, consistencyLevel, allowHints, requestTime); + } + } - Token tk = proposal.update.partitionKey().getToken(); + public static void commitPaxosTracked(Keyspace keyspace, Commit proposal, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) throws WriteTimeoutException + { + commitPaxosTracked(keyspace, proposal, consistencyLevel, requestTime, false); + } + + /** + * Commit a Paxos proposal for a tracked keyspace. + * Generates a mutation ID, sends to all replicas, and optionally waits for responses. + * + * @param respondAfterSend if true, returns immediately after sending (don't wait for application) + */ + public static void commitPaxosTracked(Keyspace keyspace, Commit proposal, ConsistencyLevel consistencyLevel, + Dispatcher.RequestTime requestTime, boolean respondAfterSend) throws WriteTimeoutException + { + boolean shouldBlock = !respondAfterSend && consistencyLevel != ConsistencyLevel.ANY; + String keyspaceName = proposal.metadata().keyspace; + Token tk = proposal.partitionKey().getToken(); + + // Generate mutation ID for tracked keyspace, preserving the commit subclass + MutationId mutationId; + if (proposal.mutation.id().isNone()) + { + mutationId = MutationTrackingService.instance.nextMutationId(keyspaceName, tk); + proposal = proposal.withMutationId(mutationId); + } + else + { + // A commit that is being resent might already have an ID read out of the system table + mutationId = proposal.mutation.id(); + } + + // NOTE: this ReplicaPlan is a lie, this usage of ReplicaPlan could do with being clarified - the selected() collection is essentially (I think) never used + ReplicaPlan.ForWrite replicaPlan = ReplicaPlans.forWrite(keyspace, consistencyLevel, tk, ReplicaPlans.writeAll); + AbstractReplicationStrategy rs = replicaPlan.replicationStrategy(); + + // If we are coordinating a new mutation id for the first time then create a TrackedWriteResponseHandler + AbstractWriteResponseHandler responseHandler = rs.getWriteResponseHandler(replicaPlan, null, WriteType.SIMPLE, null, requestTime); + responseHandler = TrackedWriteResponseHandler.wrap(responseHandler, mutationId); + + // For tracked keyspaces, the local commit MUST execute synchronously BEFORE sending to remote replicas. + // This ensures the mutation is written to the journal before any failure callback can trigger + // reconciliation via ActiveLogReconciler. + Message message = Message.outWithFlag(PAXOS_COMMIT_REQ, proposal, MessageFlag.CALL_BACK_ON_FAILURE); + Replica localReplica = null; + for (Replica replica : replicaPlan.liveAndDown()) + { + if (replica.isSelf()) + { + localReplica = replica; + break; + } + } + + // Execute local commit SYNCHRONOUSLY first to ensure journal write completes + if (localReplica != null) + { + try + { + PaxosState.commitDirect(proposal); + if (shouldBlock) + responseHandler.onResponse(null); + } + catch (Exception ex) + { + if (!(ex instanceof WriteTimeoutException)) + logger.error("Failed to apply paxos commit locally", ex); + if (shouldBlock) + responseHandler.onFailure(FBUtilities.getBroadcastAddressAndPort(), RequestFailure.forException(ex)); + } + } + + // Now send to remote replicas + IntHashSet remoteReplicas = new IntHashSet(); + for (Replica replica : replicaPlan.liveAndDown()) + { + if (replica.isSelf()) + continue; // Already executed locally above + + InetAddressAndPort destination = replica.endpoint(); + remoteReplicas.add(ClusterMetadata.current().directory.peerId(destination).id()); + + if (shouldBlock) + MessagingService.instance().sendWriteWithCallback(message, replica, responseHandler); + else + MessagingService.instance().send(message, destination); + } + + // Register write request with tracking service + if (!remoteReplicas.isEmpty()) + MutationTrackingService.instance.sentWriteRequest(proposal.makeMutation(), remoteReplicas); + + if (shouldBlock) + responseHandler.get(); + } + + private static void commitPaxosUntracked(Keyspace keyspace, Commit proposal, ConsistencyLevel consistencyLevel, boolean allowHints, Dispatcher.RequestTime requestTime) throws WriteTimeoutException + { + boolean shouldBlock = consistencyLevel != ConsistencyLevel.ANY; + PartitionUpdate update = proposal.update; + + Token tk = update.partitionKey().getToken(); AbstractWriteResponseHandler responseHandler = null; // NOTE: this ReplicaPlan is a lie, this usage of ReplicaPlan could do with being clarified - the selected() collection is essentially (I think) never used @@ -963,6 +1243,148 @@ public class StorageProxy implements StorageProxyMBean }); } + /** + * Checks if this commit needs to be forwarded to a replica coordinator for tracked keyspace support. + */ + private static boolean isTrackedKeyspaceRequiringPaxosCommitForwarding(KeyspaceMetadata ksMetadata, Commit proposal, EndpointsForToken participants) + { + if (!ksMetadata.params.replicationType.isTracked()) + return false; + + // Check if current coordinator is not a replica + InetAddressAndPort localEndpoint = FBUtilities.getBroadcastAddressAndPort(); + boolean isLocalReplica = participants.endpoints().contains(localEndpoint); + return !isLocalReplica; + } + + /** + * Forwards a Paxos V1 commit operation to a replica coordinator for tracked keyspaces. + * Uses the replica plan to select the best live, non-local replica based on proximity. + */ + private static void forwardPaxosCommit(Commit proposal, ConsistencyLevel consistencyLevel, ReplicaPlan.ForWrite replicaPlan) throws WriteTimeoutException + { + InetAddressAndPort localEndpoint = FBUtilities.getBroadcastAddressAndPort(); + + // Get live replicas and filter out local node + EndpointsForToken liveReplicas = replicaPlan.live().filter(replica -> !replica.endpoint().equals(localEndpoint)); + + if (liveReplicas.isEmpty()) + { + // No live replica available, throw exception + throw new WriteTimeoutException(WriteType.CAS, consistencyLevel, 0, consistencyLevel.blockFor(replicaPlan.replicationStrategy())); + } + + // Sort by proximity and select the best coordinator + EndpointsForToken sortedReplicas = DatabaseDescriptor.getNodeProximity().sortedByProximity(localEndpoint, liveReplicas); + InetAddressAndPort replicaCoordinator = sortedReplicas.get(0).endpoint(); + + // Create forward request with participant list + PaxosCommitForwardRequest forwardRequest = new PaxosCommitForwardRequest(proposal, consistencyLevel); + Message message = Message.out(PAXOS_COMMIT_FORWARD_REQ, forwardRequest); + + // Use AsyncPromise for proper callback handling + Promise promise = new AsyncPromise<>(); + + RequestCallback callback = new RequestCallback() + { + @Override + public void onResponse(Message response) + { + promise.setSuccess(response.payload); + } + + @Override + public void onFailure(InetAddressAndPort from, RequestFailure reason) + { + promise.setFailure(new WriteTimeoutException(WriteType.CAS, consistencyLevel, 0, consistencyLevel.blockFor(replicaPlan.replicationStrategy()))); + } + }; + + try + { + MessagingService.instance().sendWithCallback(message, replicaCoordinator, callback); + + // Wait for response with timeout + promise.get(DatabaseDescriptor.getWriteRpcTimeout(java.util.concurrent.TimeUnit.MILLISECONDS), java.util.concurrent.TimeUnit.MILLISECONDS); + } + catch (TimeoutException e) + { + throw new WriteTimeoutException(WriteType.CAS, consistencyLevel, 0, consistencyLevel.blockFor(replicaPlan.replicationStrategy())); + } + catch (Exception e) + { + if (e instanceof WriteTimeoutException) + throw (WriteTimeoutException) e; + + throw new WriteTimeoutException(WriteType.CAS, consistencyLevel, 0, consistencyLevel.blockFor(replicaPlan.replicationStrategy())); + } + } + + /** + * Performs a coordinated write with mutation tracking. + * Assumes that local coordinator is a replica (forwarding implementation pending). + * + * @param mutation + * @param consistencyLevel + * @param requestTime + */ + public static void mutateWithTracking(Mutation mutation, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) + { + try + { + TrackedWriteRequest.perform(mutation, consistencyLevel, requestTime).get(); + } + catch (WriteTimeoutException|WriteFailureException ex) + { + if (consistencyLevel == ConsistencyLevel.ANY) + { + // TODO (expected): what exactly? + } + else + { + if (ex instanceof WriteFailureException) + { + writeMetrics.failures.mark(); + writeMetricsForLevel(consistencyLevel).failures.mark(); + WriteFailureException fe = (WriteFailureException)ex; + Tracing.trace("Write failure; received {} of {} required replies, failed {} requests", + fe.received, fe.blockFor, fe.failureReasonByEndpoint.size()); + } + else + { + writeMetrics.timeouts.mark(); + writeMetricsForLevel(consistencyLevel).timeouts.mark(); + WriteTimeoutException te = (WriteTimeoutException)ex; + Tracing.trace("Write timeout; received {} of {} required replies", te.received, te.blockFor); + } + throw ex; + } + } + catch (UnavailableException e) + { + writeMetrics.unavailables.mark(); + writeMetricsForLevel(consistencyLevel).unavailables.mark(); + Tracing.trace("Unavailable"); + throw e; + } + catch (OverloadedException e) + { + writeMetrics.unavailables.mark(); + writeMetricsForLevel(consistencyLevel).unavailables.mark(); + Tracing.trace("Overloaded"); + throw e; + } + finally + { + // We track latency based on request processing time, since the amount of time that request spends in the queue + // is not a representative metric of replica performance. + long latency = nanoTime() - requestTime.startedAtNanos(); + writeMetrics.addNano(latency); + writeMetricsForLevel(consistencyLevel).addNano(latency); + updateCoordinatorWriteLatencyTableMetric(singleton(mutation), latency); + } + } + /** * Use this method to have these Mutations applied * across all replicas. This method will take care @@ -2258,9 +2680,30 @@ public class StorageProxy implements StorageProxyMBean isForWrite); } - private static PartitionIterator readWithConsensus(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) + public static PartitionIterator readWithConsensus(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) throws InvalidRequestException, UnavailableException, ReadFailureException, ReadTimeoutException { + return readWithConsensusInternal(group, consistencyLevel, requestTime, false); + } + + /** + * Version of readWithConsensus called by handlers that have already received a forwarded request. + * This prevents infinite forwarding loops if the forwarding target is not actually a replica. + */ + public static PartitionIterator readWithConsensusForwarded(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) + throws InvalidRequestException, UnavailableException, ReadFailureException, ReadTimeoutException + { + return readWithConsensusInternal(group, consistencyLevel, requestTime, true); + } + + private static PartitionIterator readWithConsensusInternal(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime, boolean alreadyForwarded) + throws InvalidRequestException, UnavailableException, ReadFailureException, ReadTimeoutException + { + // Check if this consensus read needs to be forwarded to a replica coordinator for tracked keyspaces + PartitionIterator forwardResult = checkAndForwardConsensusReadIfNeeded(group, consistencyLevel, requestTime, alreadyForwarded); + if (forwardResult != null) + return forwardResult; + ConsensusAttemptResult lastResult; do { @@ -3611,6 +4054,16 @@ public class StorageProxy implements StorageProxyMBean { return new ConsensusAttemptResult(casResult, null, false); } + + /** + * Get the CAS result row iterator. + * @return the CAS result, or null if this was not a CAS operation result + */ + @Nullable + public RowIterator getCasResult() + { + return casResult; + } } @Override @@ -3878,4 +4331,191 @@ public class StorageProxy implements StorageProxyMBean { DatabaseDescriptor.setClientRequestSizeMetricsEnabled(enabled); } + + /** + * Check if a CAS operation needs to be forwarded to a replica coordinator for tracked keyspaces. + * Returns null if no forwarding is needed, or the result of the forwarded operation. + */ + private static RowIterator checkAndForwardCasIfNeeded(String keyspaceName, + String cfName, + DecoratedKey key, + CQL3CasRequest request, + ConsistencyLevel consistencyForPaxos, + ConsistencyLevel consistencyForCommit, + ClientState clientState, + long nowInSeconds, + Dispatcher.RequestTime requestTime, + boolean alreadyForwarded) + throws UnavailableException, RequestFailureException, RequestTimeoutException + { + // Get keyspace metadata to check if it's tracked + Keyspace keyspace = Keyspace.openIfExists(keyspaceName); + if (keyspace == null) + throw new KeyspaceNotDefinedException("Keyspace " + keyspaceName + " does not exist"); + + KeyspaceMetadata ksMetadata = keyspace.getMetadata(); + if (!ksMetadata.params.replicationType.isTracked()) + return null; // Not tracked, no forwarding needed + + // Property to disable top-level forwarding for testing + if (DISABLE_CONSENSUS_REQUEST_FORWARDING) + return null; + + // Check if current coordinator is not a replica + Token tk = key.getToken(); + EndpointsForToken allReplicas = ReplicaLayout.forTokenWriteLiveAndDown(ClusterMetadata.current(), keyspace, tk) + .all(); + EndpointsForToken liveReplicas = allReplicas.filter(FailureDetector.isReplicaAlive); + + InetAddressAndPort localEndpoint = FBUtilities.getBroadcastAddressAndPort(); + boolean isLocalReplica = allReplicas.contains(localEndpoint); + + if (isLocalReplica) + return null; // Local node is a replica, no forwarding needed + + // If this request was already forwarded to us and we're not a replica, something is wrong + if (alreadyForwarded) + { + logger.error("Received forwarded CAS for keyspace {} table {} key {} but local node {} is not a replica. Replicas are: {}", + keyspaceName, cfName, key, localEndpoint, allReplicas); + Tracing.trace("ERROR: Received forwarded CAS but local node is not a replica"); + throw new InvalidRequestException("Forwarded CAS received by non-replica node " + localEndpoint); + } + + // Find best replica to forward to using proximity-based selection + if (liveReplicas.isEmpty()) + throw new UnavailableException("No live replicas available for CAS forwarding", consistencyForPaxos, 1, 0); + + // Sort by proximity and select the best coordinator + EndpointsForToken sortedReplicas = DatabaseDescriptor.getNodeProximity().sortedByProximity(localEndpoint, liveReplicas); + InetAddressAndPort replicaCoordinator = sortedReplicas.get(0).endpoint(); + + // Create forward request + CasForwardRequest forwardRequest = + new CasForwardRequest(keyspaceName, cfName, key, consistencyForPaxos, consistencyForCommit, + nowInSeconds, clientState, request); + Message message = Message.out(Verb.CAS_FORWARD_REQ, forwardRequest); + + try + { + // Send synchronous request to replica coordinator + Object responseObj = MessagingService.instance().sendWithResult(message, replicaCoordinator).get(); + @SuppressWarnings("unchecked") + Message responseMessage = (Message) responseObj; + CasForwardResponse response = responseMessage.payload; + + // Add warnings from forwarded operation to local ClientWarn + for (String warning : response.warnings) + ClientWarn.instance.warn(warning); + + // Check if the forwarded operation had an exception + if (!response.isSuccess()) + { + throw response.exception; + } + + return response.result; + } + catch (CassandraException ce) + { + // Rethrow CassandraExceptions from the replica coordinator + throw ce; + } + catch (Exception e) + { + throw new RuntimeException("Failed to forward CAS operation to replica coordinator", e); + } + } + + /** + * Check if a consensus read operation needs to be forwarded to a replica coordinator for tracked keyspaces. + * Returns null if no forwarding is needed, or the result of the forwarded operation. + */ + private static PartitionIterator checkAndForwardConsensusReadIfNeeded(SinglePartitionReadCommand.Group group, + ConsistencyLevel consistencyLevel, + Dispatcher.RequestTime requestTime, + boolean alreadyForwarded) + throws UnavailableException, ReadFailureException, ReadTimeoutException + { + if (group.queries.isEmpty()) + return null; + + // Use the first command to determine keyspace and key for replica planning + SinglePartitionReadCommand firstCommand = group.queries.get(0); + String keyspaceName = firstCommand.metadata().keyspace; + + // Get keyspace metadata to check if it's tracked + Keyspace keyspace = Keyspace.openIfExists(keyspaceName); + if (keyspace == null) + throw new KeyspaceNotDefinedException("Keyspace " + keyspaceName + " does not exist"); + + KeyspaceMetadata ksMetadata = keyspace.getMetadata(); + if (!ksMetadata.params.replicationType.isTracked()) + return null; // Not tracked, no forwarding needed + + // Property to disable top-level forwarding for testing + if (DISABLE_CONSENSUS_REQUEST_FORWARDING) + return null; + + // Check if current coordinator is not a replica + Token tk = firstCommand.partitionKey().getToken(); + EndpointsForToken allReplicas = ReplicaLayout.forTokenWriteLiveAndDown(ClusterMetadata.current(), keyspace, tk) + .all(); + EndpointsForToken liveReplicas = allReplicas.filter(FailureDetector.isReplicaAlive); + + InetAddressAndPort localEndpoint = FBUtilities.getBroadcastAddressAndPort(); + boolean isLocalReplica = allReplicas.contains(localEndpoint); + + if (isLocalReplica) + return null; // Local node is a replica, no forwarding needed + + // If this request was already forwarded to us and we're not a replica, something is wrong + if (alreadyForwarded) + { + logger.error("Received forwarded consensus read for keyspace {} key {} but local node {} is not a replica. Replicas are: {}", + keyspaceName, firstCommand.partitionKey(), localEndpoint, allReplicas); + Tracing.trace("ERROR: Received forwarded consensus read but local node is not a replica"); + throw new RuntimeException("Forwarded consensus read received by non-replica node " + localEndpoint); + } + + // Find best replica to forward to using proximity-based selection + if (liveReplicas.isEmpty()) + throw new UnavailableException("No live replicas available for consensus read forwarding", consistencyLevel, 1, 0); + + // Sort by proximity and select the best coordinator + EndpointsForToken sortedReplicas = DatabaseDescriptor.getNodeProximity().sortedByProximity(localEndpoint, liveReplicas); + InetAddressAndPort replicaCoordinator = sortedReplicas.get(0).endpoint(); + + // Create forward request - consensus reads only have a single command + ConsensusReadForwardRequest forwardRequest = new ConsensusReadForwardRequest(firstCommand, consistencyLevel); + Message message = Message.out(CONSENSUS_READ_FORWARD_REQ, forwardRequest); + + try + { + // Send synchronous request to replica coordinator + Object responseObj = MessagingService.instance().sendWithResult(message, replicaCoordinator).get(); + @SuppressWarnings("unchecked") + Message responseMessage = (Message) responseObj; + CasForwardResponse response = responseMessage.payload; + + // Add warnings from forwarded operation to local ClientWarn + for (String warning : response.warnings) + ClientWarn.instance.warn(warning); + + // Check if the forwarded operation had an exception + if (!response.isSuccess()) + throw response.exception; + + return response.partitionIterator(); + } + catch (CassandraException ce) + { + // Rethrow CassandraExceptions from the replica coordinator + throw ce; + } + catch (Exception e) + { + throw new RuntimeException("Failed to forward consensus read operation to replica coordinator", e); + } + } } diff --git a/src/java/org/apache/cassandra/service/TrackedWriteResponseHandler.java b/src/java/org/apache/cassandra/service/TrackedWriteResponseHandler.java index 38e4480c9d..f961fceb01 100644 --- a/src/java/org/apache/cassandra/service/TrackedWriteResponseHandler.java +++ b/src/java/org/apache/cassandra/service/TrackedWriteResponseHandler.java @@ -19,37 +19,37 @@ package org.apache.cassandra.service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.apache.cassandra.exceptions.RequestFailure; import org.apache.cassandra.exceptions.WriteFailureException; import org.apache.cassandra.exceptions.WriteTimeoutException; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.Message; -import org.apache.cassandra.net.NoPayload; import org.apache.cassandra.replication.MutationId; import org.apache.cassandra.replication.MutationTrackingService; -public class TrackedWriteResponseHandler extends AbstractWriteResponseHandler +public class TrackedWriteResponseHandler extends AbstractWriteResponseHandler { private static final Logger logger = LoggerFactory.getLogger(TrackedWriteResponseHandler.class); - private final AbstractWriteResponseHandler wrapped; + private final AbstractWriteResponseHandler wrapped; private final MutationId mutationId; - private TrackedWriteResponseHandler(AbstractWriteResponseHandler wrapped, MutationId mutationId) + private TrackedWriteResponseHandler(AbstractWriteResponseHandler wrapped, MutationId mutationId) { super(wrapped.replicaPlan, wrapped.callback, wrapped.writeType, null, wrapped.getRequestTime()); this.wrapped = wrapped; this.mutationId = mutationId; } - public static TrackedWriteResponseHandler wrap(AbstractWriteResponseHandler handler, MutationId mutationId) + public static TrackedWriteResponseHandler wrap(AbstractWriteResponseHandler handler, MutationId mutationId) { - return new TrackedWriteResponseHandler(handler, mutationId); + return new TrackedWriteResponseHandler<>(handler, mutationId); } @Override - public void onResponse(Message msg) + public void onResponse(Message msg) { // Local mutations are witnessed from Keyspace.applyInternalTracked if (msg != null) diff --git a/src/java/org/apache/cassandra/service/accord/exceptions/AccordReadExhaustedException.java b/src/java/org/apache/cassandra/service/accord/exceptions/AccordReadExhaustedException.java index 697b31d6e3..a2f8e7cadf 100644 --- a/src/java/org/apache/cassandra/service/accord/exceptions/AccordReadExhaustedException.java +++ b/src/java/org/apache/cassandra/service/accord/exceptions/AccordReadExhaustedException.java @@ -18,7 +18,13 @@ package org.apache.cassandra.service.accord.exceptions; +import java.io.IOException; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.exceptions.CassandraExceptionCode; import org.apache.cassandra.exceptions.ReadTimeoutException; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; import static org.apache.cassandra.db.ConsistencyLevel.SERIAL; @@ -33,4 +39,35 @@ public class AccordReadExhaustedException extends ReadTimeoutException { super(SERIAL, received, blockFor, dataPresent, msg); } + + @Override + protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException + { + // Only serialize the fields that vary - consistency is always SERIAL + out.writeUnsignedVInt32(received); + out.writeUnsignedVInt32(blockFor); + out.writeBoolean(dataPresent); + } + + @Override + protected long serializedSizeSpecificFields(int version) + { + return TypeSizes.sizeofUnsignedVInt(received) + + TypeSizes.sizeofUnsignedVInt(blockFor) + + TypeSizes.BOOL_SIZE; // dataPresent + } + + public static AccordReadExhaustedException deserializeFields(String message, DataInputPlus in, int version) throws IOException + { + int received = in.readUnsignedVInt32(); + int blockFor = in.readUnsignedVInt32(); + boolean dataPresent = in.readBoolean(); + return new AccordReadExhaustedException(received, blockFor, dataPresent, message); + } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.ACCORD_READ_EXHAUSTED; + } } diff --git a/src/java/org/apache/cassandra/service/accord/exceptions/AccordReadPreemptedException.java b/src/java/org/apache/cassandra/service/accord/exceptions/AccordReadPreemptedException.java index 53b37b7c89..4389f3781f 100644 --- a/src/java/org/apache/cassandra/service/accord/exceptions/AccordReadPreemptedException.java +++ b/src/java/org/apache/cassandra/service/accord/exceptions/AccordReadPreemptedException.java @@ -18,7 +18,13 @@ package org.apache.cassandra.service.accord.exceptions; +import java.io.IOException; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.exceptions.CassandraExceptionCode; import org.apache.cassandra.exceptions.ReadTimeoutException; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; import static org.apache.cassandra.db.ConsistencyLevel.SERIAL; @@ -34,4 +40,35 @@ public class AccordReadPreemptedException extends ReadTimeoutException { super(SERIAL, received, blockFor, dataPresent, msg); } + + @Override + protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException + { + // Only serialize the fields that vary - consistency is always SERIAL + out.writeUnsignedVInt32(received); + out.writeUnsignedVInt32(blockFor); + out.writeBoolean(dataPresent); + } + + @Override + protected long serializedSizeSpecificFields(int version) + { + return TypeSizes.sizeofUnsignedVInt(received) + + TypeSizes.sizeofUnsignedVInt(blockFor) + + TypeSizes.BOOL_SIZE; // dataPresent + } + + public static AccordReadPreemptedException deserializeFields(String message, DataInputPlus in, int version) throws IOException + { + int received = in.readUnsignedVInt32(); + int blockFor = in.readUnsignedVInt32(); + boolean dataPresent = in.readBoolean(); + return new AccordReadPreemptedException(received, blockFor, dataPresent, message); + } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.ACCORD_READ_PREEMPTED; + } } diff --git a/src/java/org/apache/cassandra/service/accord/exceptions/AccordWriteExhaustedException.java b/src/java/org/apache/cassandra/service/accord/exceptions/AccordWriteExhaustedException.java index 6aca227240..9d8ea6b7fd 100644 --- a/src/java/org/apache/cassandra/service/accord/exceptions/AccordWriteExhaustedException.java +++ b/src/java/org/apache/cassandra/service/accord/exceptions/AccordWriteExhaustedException.java @@ -18,8 +18,14 @@ package org.apache.cassandra.service.accord.exceptions; +import java.io.IOException; + +import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.WriteType; +import org.apache.cassandra.exceptions.CassandraExceptionCode; import org.apache.cassandra.exceptions.WriteTimeoutException; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; import static org.apache.cassandra.db.ConsistencyLevel.SERIAL; @@ -34,4 +40,32 @@ public class AccordWriteExhaustedException extends WriteTimeoutException { super(WriteType.CAS, SERIAL, received, blockFor, msg); } + + @Override + protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException + { + // Only serialize the fields that vary - consistency is always SERIAL, writeType is always CAS + out.writeUnsignedVInt32(received); + out.writeUnsignedVInt32(blockFor); + } + + @Override + protected long serializedSizeSpecificFields(int version) + { + return TypeSizes.sizeofUnsignedVInt(received) + + TypeSizes.sizeofUnsignedVInt(blockFor); + } + + public static AccordWriteExhaustedException deserializeFields(String message, DataInputPlus in, int version) throws IOException + { + int received = in.readUnsignedVInt32(); + int blockFor = in.readUnsignedVInt32(); + return new AccordWriteExhaustedException(received, blockFor, message); + } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.ACCORD_WRITE_EXHAUSTED; + } } diff --git a/src/java/org/apache/cassandra/service/accord/exceptions/AccordWritePreemptedException.java b/src/java/org/apache/cassandra/service/accord/exceptions/AccordWritePreemptedException.java index ee02e6dfad..f65c4a10db 100644 --- a/src/java/org/apache/cassandra/service/accord/exceptions/AccordWritePreemptedException.java +++ b/src/java/org/apache/cassandra/service/accord/exceptions/AccordWritePreemptedException.java @@ -18,8 +18,14 @@ package org.apache.cassandra.service.accord.exceptions; +import java.io.IOException; + +import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.WriteType; +import org.apache.cassandra.exceptions.CassandraExceptionCode; import org.apache.cassandra.exceptions.WriteTimeoutException; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; import static org.apache.cassandra.db.ConsistencyLevel.SERIAL; @@ -35,4 +41,32 @@ public class AccordWritePreemptedException extends WriteTimeoutException { super(WriteType.CAS, SERIAL, received, blockFor, msg); } + + @Override + protected void serializeSpecificFields(DataOutputPlus out, int version) throws IOException + { + // Only serialize the fields that vary - consistency is always SERIAL, writeType is always CAS + out.writeUnsignedVInt32(received); + out.writeUnsignedVInt32(blockFor); + } + + @Override + protected long serializedSizeSpecificFields(int version) + { + return TypeSizes.sizeofUnsignedVInt(received) + + TypeSizes.sizeofUnsignedVInt(blockFor); + } + + public static AccordWritePreemptedException deserializeFields(String message, DataInputPlus in, int version) throws IOException + { + int received = in.readUnsignedVInt32(); + int blockFor = in.readUnsignedVInt32(); + return new AccordWritePreemptedException(received, blockFor, message); + } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.ACCORD_WRITE_PREEMPTED; + } } diff --git a/src/java/org/apache/cassandra/service/accord/txn/TxnReferenceOperations.java b/src/java/org/apache/cassandra/service/accord/txn/TxnReferenceOperations.java index 8063e0eed3..4e2c970fbd 100644 --- a/src/java/org/apache/cassandra/service/accord/txn/TxnReferenceOperations.java +++ b/src/java/org/apache/cassandra/service/accord/txn/TxnReferenceOperations.java @@ -60,13 +60,17 @@ public class TxnReferenceOperations if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TxnReferenceOperations that = (TxnReferenceOperations) o; + + if (this.isEmpty() && that.isEmpty()) + return true; + return metadata.equals(that.metadata) && clusterings.equals(that.clusterings) && regulars.equals(that.regulars) && statics.equals(that.statics); } @Override public int hashCode() { - return Objects.hash(metadata, clusterings, regulars, statics); + return Objects.hash(regulars, statics); } @Override @@ -85,12 +89,35 @@ public class TxnReferenceOperations return regulars.isEmpty() && statics.isEmpty(); } + /** + * Public accessors for CAS forwarding serialization support. + * These enable reuse of TxnReferenceOperations structure without exposing internal implementation. + */ + public List> getClusterings() + { + return clusterings; + } + + public List getRegulars() + { + return regulars; + } + + public List getStatics() + { + return statics; + } + static final ParameterisedVersionedSerializer serializer = new ParameterisedVersionedSerializer<>() { + private static final int HAS_CONTENT = 0x01; + @Override public void serialize(TxnReferenceOperations operations, TableMetadatas tables, DataOutputPlus out, Version version) throws IOException { - out.writeBoolean(!operations.isEmpty()); + int flags = (!operations.isEmpty() ? HAS_CONTENT : 0); + out.write(flags); + if (operations.isEmpty()) return; @@ -105,7 +132,10 @@ public class TxnReferenceOperations @Override public TxnReferenceOperations deserialize(TableMetadatas tables, DataInputPlus in, Version version) throws IOException { - if (!in.readBoolean()) + int flags = in.readUnsignedByte(); + boolean hasContent = (flags & HAS_CONTENT) != 0; + + if (!hasContent) return TxnReferenceOperations.empty(); TableMetadata metadata = tables.deserialize(in); @@ -120,7 +150,7 @@ public class TxnReferenceOperations @Override public long serializedSize(TxnReferenceOperations operations, TableMetadatas tables, Version version) { - long size = TypeSizes.BOOL_SIZE; + long size = TypeSizes.BYTE_SIZE; // flags byte if (operations.isEmpty()) return size; size += tables.serializedSize(operations.metadata); @@ -128,19 +158,8 @@ public class TxnReferenceOperations for (Clustering clustering : operations.clusterings) size += Clustering.serializer.serializedSize(clustering, version.messageVersion(), operations.metadata.comparator.subtypes()); size += serializedListSize(operations.regulars, tables, TxnReferenceOperation.serializer); - size += serializedListSize(operations.statics, tables, TxnReferenceOperation.serializer); + size += serializedListSize(operations.statics, tables, TxnReferenceOperation.serializer); return size; } - - private TableMetadatas tables(TxnReferenceOperations operations) - { - TableMetadatas.Collector collector = new TableMetadatas.Collector(); - collector.add(operations.metadata); - for (TxnReferenceOperation op : operations.regulars) - op.collect(collector); - for (TxnReferenceOperation op : operations.statics) - op.collect(collector); - return collector.build(); - } }; } 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 d74d5a0608..d7add3fdef 100644 --- a/src/java/org/apache/cassandra/service/accord/txn/TxnWrite.java +++ b/src/java/org/apache/cassandra/service/accord/txn/TxnWrite.java @@ -24,7 +24,9 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; @@ -47,16 +49,23 @@ import accord.utils.SimpleBitSets; import accord.utils.async.AsyncChain; import accord.utils.async.AsyncChains; +import org.apache.cassandra.cql3.QueryOptions; import org.apache.cassandra.cql3.UpdateParameters; import org.apache.cassandra.db.Clustering; import org.apache.cassandra.db.Columns; import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionTime; import org.apache.cassandra.db.Mutation; +import org.apache.cassandra.db.RangeTombstone; import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.RegularAndStaticColumns; import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.db.marshal.TimeUUIDType; +import org.apache.cassandra.db.partitions.FilteredPartition; +import org.apache.cassandra.db.partitions.Partition; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.io.ParameterisedVersionedSerializer; import org.apache.cassandra.io.util.DataInputBuffer; import org.apache.cassandra.io.util.DataInputPlus; @@ -64,15 +73,19 @@ import org.apache.cassandra.io.util.DataOutputBuffer; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.replication.MutationId; import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.accord.AccordCommandStore; import org.apache.cassandra.service.accord.AccordExecutor; import org.apache.cassandra.service.accord.api.PartitionKey; import org.apache.cassandra.service.accord.serializers.TableMetadatas; import org.apache.cassandra.service.accord.serializers.TableMetadatasAndKeys; import org.apache.cassandra.service.accord.serializers.Version; +import org.apache.cassandra.service.paxos.Ballot; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.ObjectSizes; import org.apache.cassandra.utils.SimpleBitSetSerializers; +import org.apache.cassandra.utils.TimeUUID; import static com.google.common.base.Preconditions.checkState; import static org.apache.cassandra.db.rows.DeserializationHelper.Flag.FROM_REMOTE; @@ -324,6 +337,56 @@ public class TxnWrite extends AbstractKeySorted implements Writ return new Update(this.key, index, updateBuilder.build(), tables); } + public void completeToBuilder(PartitionUpdate.Builder updateBuilder, TxnData data, Ballot ballot, FilteredPartition current, ClientState clientState) throws InvalidRequestException + { + if (isComplete()) + { + // No reference operations - add base update components directly + if (!baseUpdate.staticRow().isEmpty()) + updateBuilder.add(baseUpdate.staticRow()); + + for (Row row : baseUpdate) + updateBuilder.add(row); + + // Copy deletion info (partition deletion and range tombstones) + // Timestamps will be updated later in Proposal.of() via updateAllTimestamp() + DeletionTime partitionDeletion = baseUpdate.deletionInfo().getPartitionDeletion(); + if (!partitionDeletion.isLive()) + updateBuilder.addPartitionDeletion(partitionDeletion); + + Iterator rangeIterator = baseUpdate.deletionInfo().rangeIterator(false); + while (rangeIterator.hasNext()) + updateBuilder.add(rangeIterator.next()); + + return; + } + + // Create CAS-specific UpdateParameters with prefetched data and transaction timestamp + long transactionTimestamp = ballot.unixMicros(); + Map prefetchedRows = Collections.singletonMap( + baseUpdate.partitionKey(), current); + + CasUpdateParameters up = new CasUpdateParameters( + baseUpdate.metadata(), clientState, QueryOptions.DEFAULT, + transactionTimestamp, (int)(transactionTimestamp / 1000), 0, + prefetchedRows, ballot.msb(), 0); + + // Apply static operations + Row staticRow = applyUpdates(baseUpdate.staticRow(), referenceOps.getStatics(), + baseUpdate.partitionKey(), Clustering.STATIC_CLUSTERING, up, data); + if (!staticRow.isEmpty()) + updateBuilder.add(staticRow); + + // Apply regular operations + for (Clustering clustering : referenceOps.clusterings) + { + Row existing = baseUpdate.hasRows() ? baseUpdate.getRow(clustering) : null; + Row row = applyUpdates(existing, referenceOps.regulars, baseUpdate.partitionKey(), clustering, up, data); + if (row != null) + updateBuilder.add(row); + } + } + private static Columns columns(Columns current, List referenceOps) { if (referenceOps.isEmpty()) @@ -370,8 +433,8 @@ public class TxnWrite extends AbstractKeySorted implements Writ return up.buildRow(); } - static final FragmentSerializer serializer = new FragmentSerializer(); - static class FragmentSerializer + public static final FragmentSerializer serializer = new FragmentSerializer(); + public static class FragmentSerializer { public void serialize(Fragment fragment, TableMetadatas tables, DataOutputPlus out, Version version) throws IOException { @@ -433,6 +496,31 @@ public class TxnWrite extends AbstractKeySorted implements Writ } } + /** + * CAS-specific UpdateParameters for CAS-specific TimeUUID handling + */ + private static class CasUpdateParameters extends UpdateParameters + { + private final long timeUuidMsb; + private long timeUuidNanos; + + public CasUpdateParameters(TableMetadata metadata, ClientState state, QueryOptions options, + long timestamp, long nowInSec, int ttl, + Map prefetchedRows, + long timeUuidMsb, long timeUuidNanos) throws InvalidRequestException + { + super(metadata, state, options, timestamp, nowInSec, ttl, prefetchedRows); + this.timeUuidMsb = timeUuidMsb; + this.timeUuidNanos = timeUuidNanos; + } + + @Override + public byte[] nextTimeUUIDAsBytes() + { + return TimeUUID.toBytes(timeUuidMsb, TimeUUIDType.signedBytesToNativeLong(timeUuidNanos++)); + } + } + public final TableMetadatas tables; private final SimpleBitSet conditionalBlockBitSet; diff --git a/src/java/org/apache/cassandra/service/paxos/CasForwardHandler.java b/src/java/org/apache/cassandra/service/paxos/CasForwardHandler.java new file mode 100644 index 0000000000..b550b5d07f --- /dev/null +++ b/src/java/org/apache/cassandra/service/paxos/CasForwardHandler.java @@ -0,0 +1,124 @@ +/* + * 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.paxos; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.rows.RowIterator; +import org.apache.cassandra.exceptions.CassandraException; +import org.apache.cassandra.exceptions.RequestFailure; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.service.ClientWarn; +import org.apache.cassandra.service.StorageProxy; +import org.apache.cassandra.tracing.Tracing; +import org.apache.cassandra.transport.Dispatcher; + +/** + * Handler for forwarded CAS (Compare-And-Set) operations. + * Executes the CAS operation on behalf of the original coordinator, + * ensuring that MutationId generation happens on a replica coordinator for tracked keyspaces. + * + * TODO (expected): more comprehensive testing + */ +public class CasForwardHandler implements IVerbHandler +{ + public static final CasForwardHandler instance = new CasForwardHandler(); + private static final Logger logger = LoggerFactory.getLogger(CasForwardHandler.class); + + @Override + public void doVerb(Message message) + { + CasForwardRequest request = message.payload; + + Tracing.trace("Executing forwarded CAS operation for {}", request.key); + + // Start capturing client warnings for the forwarded operation + ClientWarn.instance.captureWarnings(); + try + { + // Validate keyspace exists and is tracked + KeyspaceMetadata ksMetadata = Schema.instance.getKeyspaceMetadata(request.keyspaceName); + if (ksMetadata == null) + { + MessagingService.instance().respondWithFailure(RequestFailureReason.INCOMPATIBLE_SCHEMA, message); + logger.error("Failed to forward CAS operation for non-existent keyspace {}", request.keyspaceName); + return; + } + + if (!ksMetadata.params.replicationType.isTracked()) + { + MessagingService.instance().respondWithFailure(RequestFailureReason.INCOMPATIBLE_SCHEMA, message); + logger.error("Asked to perform forwarded CAS operation, but keyspace {} is not tracked", request.keyspaceName); + return; + } + + // Execute the forwarded CAS operation + logger.debug("Executing CAS operation for table {}.{} with key {}", + request.keyspaceName, request.cfName, request.key); + + // Execute the CAS request using StorageProxy with the forwarded client state + Dispatcher.RequestTime requestTime = Dispatcher.RequestTime.forImmediateExecution(); + RowIterator result = StorageProxy.casForwarded(request.keyspaceName, + request.cfName, + request.key, + request.casRequest, + request.consistencyForPaxos, + request.consistencyForCommit, + request.clientState, + request.nowInSeconds, + requestTime); + + // Create response with the CAS result and captured warnings + List warnings = ClientWarn.instance.getWarnings(); + CasForwardResponse response = new CasForwardResponse(result, warnings); + MessagingService.instance().respond(response, message); + logger.debug("Completed forwarded CAS operation for {}", request.key); + } + catch (CassandraException ce) + { + // Forward the exception back to the original coordinator with warnings + List warnings = ClientWarn.instance.getWarnings(); + CasForwardResponse response = new CasForwardResponse(ce, warnings); + MessagingService.instance().respond(response, message); + } + catch (Throwable t) + { + try + { + MessagingService.instance().respondWithFailure(RequestFailure.forException(t), message); + } + finally + { + throw t; + } + } + finally + { + ClientWarn.instance.resetWarnings(); + } + } +} diff --git a/src/java/org/apache/cassandra/service/paxos/CasForwardRequest.java b/src/java/org/apache/cassandra/service/paxos/CasForwardRequest.java new file mode 100644 index 0000000000..7b3abb3d1d --- /dev/null +++ b/src/java/org/apache/cassandra/service/paxos/CasForwardRequest.java @@ -0,0 +1,140 @@ +/* + * 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.paxos; + +import java.io.IOException; + +import org.apache.cassandra.cql3.statements.CQL3CasRequest; +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.service.ClientState; + +/** + * Request to forward a CAS operation to a replica coordinator for tracked keyspaces. + * Contains the essential information needed to execute the CAS operation on the remote coordinator. + */ +public class CasForwardRequest +{ + public static final Serializer serializer = new Serializer(); + + public final String keyspaceName; + public final String cfName; + public final DecoratedKey key; + public final ConsistencyLevel consistencyForPaxos; + public final ConsistencyLevel consistencyForCommit; + public final long nowInSeconds; + public final ClientState clientState; + public final CQL3CasRequest casRequest; // The actual CAS request to forward + + public CasForwardRequest(String keyspaceName, + String cfName, + DecoratedKey key, + ConsistencyLevel consistencyForPaxos, + ConsistencyLevel consistencyForCommit, + long nowInSeconds, + ClientState clientState, + CQL3CasRequest casRequest) + { + this.keyspaceName = keyspaceName; + this.cfName = cfName; + this.key = key; + this.consistencyForPaxos = consistencyForPaxos; + this.consistencyForCommit = consistencyForCommit; + this.nowInSeconds = nowInSeconds; + this.clientState = clientState; + this.casRequest = casRequest; + } + + public static class Serializer implements IVersionedSerializer + { + @Override + public void serialize(CasForwardRequest forwardRequest, DataOutputPlus out, int version) throws IOException + { + out.writeUTF(forwardRequest.keyspaceName); + out.writeUTF(forwardRequest.cfName); + DecoratedKey.serializer.serialize(forwardRequest.key, out, version); + out.writeByte(forwardRequest.consistencyForPaxos.code); + out.writeByte(forwardRequest.consistencyForCommit.code); + out.writeUnsignedVInt(forwardRequest.nowInSeconds); + serializeClientState(forwardRequest.clientState, out); + CQL3CasRequest.serializer.serialize(forwardRequest.casRequest, out, version); + } + + @Override + public CasForwardRequest deserialize(DataInputPlus in, int version) throws IOException + { + String keyspaceName = in.readUTF(); + String cfName = in.readUTF(); + DecoratedKey key = (DecoratedKey) DecoratedKey.serializer.deserialize(in, version); + ConsistencyLevel consistencyForPaxos = ConsistencyLevel.fromCode(in.readUnsignedByte()); + ConsistencyLevel consistencyForCommit = ConsistencyLevel.fromCode(in.readUnsignedByte()); + long nowInSeconds = in.readUnsignedVInt(); + ClientState clientState = deserializeClientState(in); + CQL3CasRequest casRequest = CQL3CasRequest.serializer.deserialize(in, version); + + return new CasForwardRequest(keyspaceName, cfName, key, consistencyForPaxos, consistencyForCommit, + nowInSeconds, clientState, casRequest); + } + + @Override + public long serializedSize(CasForwardRequest forwardRequest, int version) + { + long size = 0; + size += TypeSizes.sizeof(forwardRequest.keyspaceName); + size += TypeSizes.sizeof(forwardRequest.cfName); + size += DecoratedKey.serializer.serializedSize(forwardRequest.key, version); + size += 1; // consistencyForPaxos.code + size += 1; // consistencyForCommit.code + size += TypeSizes.sizeofUnsignedVInt(forwardRequest.nowInSeconds); + size += serializedSizeClientState(forwardRequest.clientState); + size += CQL3CasRequest.serializer.serializedSize(forwardRequest.casRequest, version); + return size; + } + + private static final int IS_SUPER = 0x01; + private static final int IS_INTERNAL = 0x02; + private static final int APPLY_GUARDRAILS = 0x04; + + private static void serializeClientState(ClientState state, DataOutputPlus out) throws IOException + { + int flags = (state.isSuper() ? IS_SUPER : 0) + | (state.isInternal ? IS_INTERNAL : 0) + | (state.applyGuardrails() ? APPLY_GUARDRAILS : 0) + ; + out.write(flags); + } + + private static ClientState deserializeClientState(DataInputPlus in) throws IOException + { + int flags = in.readUnsignedByte(); + boolean isSuper = (flags & IS_SUPER) != 0; + boolean isInternal = (flags & IS_INTERNAL) != 0; + boolean applyGuardrails = (flags & APPLY_GUARDRAILS) != 0; + return ClientState.forForwardedCalls(isInternal, applyGuardrails, isSuper); + } + + private static long serializedSizeClientState(ClientState state) + { + return 1; + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/service/paxos/CasForwardResponse.java b/src/java/org/apache/cassandra/service/paxos/CasForwardResponse.java new file mode 100644 index 0000000000..5d31a0abb0 --- /dev/null +++ b/src/java/org/apache/cassandra/service/paxos/CasForwardResponse.java @@ -0,0 +1,182 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.service.paxos; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nonnull; + +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.PartitionIterators; +import org.apache.cassandra.db.rows.RowIterator; +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.exceptions.CassandraException; +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.Schema; +import org.apache.cassandra.schema.TableId; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.CollectionSerializers; + +import static org.apache.cassandra.db.SerializationHeader.StableHeaderSerializer.STABLE; +import static org.apache.cassandra.db.rows.DeserializationHelper.Flag.FROM_REMOTE; + +/** + * Response containing the result of a forwarded CAS operation. + * Can contain either a successful result or an exception that occurred during execution. + */ +public class CasForwardResponse +{ + public final RowIterator result; + public final CassandraException exception; + + @Nonnull + public final List warnings; + + public CasForwardResponse(RowIterator result, List warnings) + { + this(result, null, warnings); + } + + public CasForwardResponse(PartitionIterator result, List warnings) + { + // Extract the single partition from the iterator (consensus reads are single partition) + this(result != null && result.hasNext() ? result.next() : null, null, warnings); + } + + public CasForwardResponse(CassandraException exception, List warnings) + { + this(null, exception, warnings); + } + + private CasForwardResponse(RowIterator result, CassandraException exception, List warnings) + { + this.result = result; + this.exception = exception; + this.warnings = warnings == null ? Collections.emptyList() : warnings; + } + + public boolean isSuccess() + { + return exception == null; + } + + /** + * Get the result as a PartitionIterator. + */ + public PartitionIterator partitionIterator() + { + return result == null ? null : PartitionIterators.singletonIterator(result); + } + + public static final Serializer serializer = new Serializer(); + + public static class Serializer implements IVersionedSerializer + { + private static final int HAS_RESULT = 0x01; + private static final int HAS_EXCEPTION = 0x02; + private static final int HAS_WARNINGS = 0x04; + + @Override + public void serialize(CasForwardResponse response, DataOutputPlus out, int version) throws IOException + { + int flags = (response.result != null ? HAS_RESULT : 0) + | (response.exception != null ? HAS_EXCEPTION : 0) + | (!response.warnings.isEmpty() ? HAS_WARNINGS : 0) + ; + out.write(flags); + + if (response.result != null) + { + FilteredPartition partition = new FilteredPartition(response.result); + partition.metadata().id.serializeCompact(out); + try (UnfilteredRowIterator iterator = partition.unfilteredIterator()) + { + UnfilteredRowIteratorSerializer.serializer.serialize(iterator, out, version, partition.rowCount(), STABLE, null); + } + } + + if (response.exception != null) + CassandraException.serializer.serialize(response.exception, out, version); + + if (!response.warnings.isEmpty()) + CollectionSerializers.serializeList(response.warnings, out, version, CollectionSerializers.STRING_SERIALIZER); + } + + @Override + public CasForwardResponse deserialize(DataInputPlus in, int version) throws IOException + { + int flags = in.readUnsignedByte(); + boolean hasResult = (flags & HAS_RESULT) != 0; + boolean hasException = (flags & HAS_EXCEPTION) != 0; + boolean hasWarnings = (flags & HAS_WARNINGS) != 0; + + RowIterator result = null; + if (hasResult) + { + TableMetadata metadata = Schema.instance.getExistingTableMetadata(TableId.deserializeCompact(in)); + UnfilteredRowIteratorSerializer.Header header = UnfilteredRowIteratorSerializer.serializer.deserializeHeader(metadata, in, version, FROM_REMOTE, STABLE, null); + try (UnfilteredRowIterator partition = UnfilteredRowIteratorSerializer.serializer.deserialize(in, version, metadata, FROM_REMOTE, header)) + { + result = UnfilteredRowIterators.filter(partition, 0); + } + } + + CassandraException exception = null; + if (hasException) + exception = CassandraException.serializer.deserialize(in, version); + + List warnings = Collections.emptyList(); + if (hasWarnings) + warnings = CollectionSerializers.deserializeList(in, version, CollectionSerializers.STRING_SERIALIZER); + + return new CasForwardResponse(result, exception, warnings); + } + + @Override + public long serializedSize(CasForwardResponse response, int version) + { + long size = TypeSizes.BYTE_SIZE; // flags byte + + if (response.result != null) + { + FilteredPartition partition = new FilteredPartition(response.result); + size += partition.metadata().id.serializedCompactSize(); + try (UnfilteredRowIterator iterator = partition.unfilteredIterator()) + { + size += UnfilteredRowIteratorSerializer.serializer.serializedSize(iterator, version, partition.rowCount(), STABLE, null); + } + } + + if (response.exception != null) + size += CassandraException.serializer.serializedSize(response.exception, version); + + if (!response.warnings.isEmpty()) + size += CollectionSerializers.serializedListSize(response.warnings, version, CollectionSerializers.STRING_SERIALIZER); + + return size; + } + } +} diff --git a/src/java/org/apache/cassandra/service/paxos/Commit.java b/src/java/org/apache/cassandra/service/paxos/Commit.java index 896beabdf2..43ab2103e1 100644 --- a/src/java/org/apache/cassandra/service/paxos/Commit.java +++ b/src/java/org/apache/cassandra/service/paxos/Commit.java @@ -33,9 +33,11 @@ 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.db.rows.EncodingStats; import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.replication.MutationId; import org.apache.cassandra.schema.TableMetadata; @@ -51,17 +53,22 @@ public class Commit { enum CompareResult { SAME, BEFORE, AFTER, IS_REPROPOSAL, WAS_REPROPOSED_BY} - public static final CommitSerializer serializer = new CommitSerializer<>(Commit::new); + public static final CommitSerializer serializer = new CommitSerializer<>(Commit::new, Commit::new); public static class Proposal extends Commit { - public static final CommitSerializer serializer = new CommitSerializer<>(Proposal::new); + public static final CommitSerializer serializer = new CommitSerializer<>(Proposal::new, Proposal::new); public Proposal(Ballot ballot, PartitionUpdate update) { super(ballot, update); } + public Proposal(Ballot ballot, Mutation mutation) + { + super(ballot, mutation); + } + public String toString() { return toString("Proposal"); @@ -80,18 +87,24 @@ public class Commit public Accepted accepted() { - return new Accepted(ballot, update); + return new Accepted(ballot, mutation); } public Agreed agreed() { - return new Agreed(ballot, update); + return new Agreed(ballot, mutation); + } + + @Override + public Proposal withMutationId(MutationId mutationId) + { + return new Proposal(ballot, makeMutation(mutationId)); } } public static class Accepted extends Proposal { - public static final CommitSerializer serializer = new CommitSerializer<>(Accepted::new); + public static final CommitSerializer serializer = new CommitSerializer<>(Accepted::new, Accepted::new); public static Accepted none(DecoratedKey partitionKey, TableMetadata metadata) { @@ -103,14 +116,19 @@ public class Commit super(ballot, update); } + public Accepted(Ballot ballot, Mutation mutation) + { + super(ballot, mutation); + } + public Accepted(Commit commit) { - super(commit.ballot, commit.update); + super(commit.ballot, commit.mutation); } Committed committed() { - return new Committed(ballot, update); + return new Committed(ballot, mutation); } boolean isExpired(long nowInSec) @@ -133,13 +151,19 @@ public class Commit return c > 0 ? a : b; return a instanceof AcceptedWithTTL ? ((AcceptedWithTTL)a).lastDeleted(b) : a; } + + @Override + public Accepted withMutationId(MutationId mutationId) + { + return new Accepted(ballot, makeMutation(mutationId)); + } } public static class AcceptedWithTTL extends Accepted { public static AcceptedWithTTL withDefaultTTL(Commit copy) { - return new AcceptedWithTTL(copy, nowInSeconds() + legacyPaxosTtlSec(copy.update.metadata())); + return new AcceptedWithTTL(copy, nowInSeconds() + legacyPaxosTtlSec(copy.metadata())); } public final long localDeletionTime; @@ -166,27 +190,44 @@ public class Commit return b instanceof AcceptedWithTTL && localDeletionTime >= ((AcceptedWithTTL) b).localDeletionTime ? this : b; } + + @Override + public AcceptedWithTTL withMutationId(MutationId mutationId) + { + return new AcceptedWithTTL(ballot, makeMutation(mutationId).getOnlyUpdate(), localDeletionTime); + } } // might prefer to call this Commit, but would mean refactoring more legacy code public static class Agreed extends Accepted { - public static final CommitSerializer serializer = new CommitSerializer<>(Agreed::new); + public static final CommitSerializer serializer = new CommitSerializer<>(Agreed::new, Agreed::new); public Agreed(Ballot ballot, PartitionUpdate update) { super(ballot, update); } + public Agreed(Ballot ballot, Mutation mutation) + { + super(ballot, mutation); + } + public Agreed(Commit copy) { super(copy); } + + @Override + public Agreed withMutationId(MutationId mutationId) + { + return new Agreed(ballot, makeMutation(mutationId)); + } } public static class Committed extends Agreed { - public static final CommitSerializer serializer = new CommitSerializer<>(Committed::new); + public static final CommitSerializer serializer = new CommitSerializer<>(Committed::new, Committed::new); public static Committed none(DecoratedKey partitionKey, TableMetadata metadata) { @@ -198,6 +239,11 @@ public class Commit super(ballot, update); } + public Committed(Ballot ballot, Mutation mutation) + { + super(ballot, mutation); + } + public Committed(Commit copy) { super(copy); @@ -220,13 +266,19 @@ public class Commit { return ballot.equals(Ballot.none()) && update.isEmpty(); } + + @Override + public Committed withMutationId(MutationId mutationId) + { + return new Committed(ballot, makeMutation(mutationId)); + } } public static class CommittedWithTTL extends Committed { public static CommittedWithTTL withDefaultTTL(Commit copy) { - return new CommittedWithTTL(copy, nowInSeconds() + legacyPaxosTtlSec(copy.update.metadata())); + return new CommittedWithTTL(copy, nowInSeconds() + legacyPaxosTtlSec(copy.metadata())); } public final long localDeletionTime; @@ -237,6 +289,12 @@ public class Commit this.localDeletionTime = localDeletionTime; } + public CommittedWithTTL(Ballot ballot, Mutation mutation, long localDeletionTime) + { + super(ballot, mutation); + this.localDeletionTime = localDeletionTime; + } + public CommittedWithTTL(Commit copy, long localDeletionTime) { super(copy); @@ -253,20 +311,62 @@ public class Commit return b instanceof CommittedWithTTL && localDeletionTime >= ((CommittedWithTTL) b).localDeletionTime ? this : b; } + + @Override + public CommittedWithTTL withMutationId(MutationId mutationId) + { + return new CommittedWithTTL(ballot, makeMutation(mutationId), localDeletionTime); + } + } + + public interface Commitable + { + enum CommitableKind + { + PARTITION_UPDATE, MUTATION + } + + CommitableKind commitableKind(); } public final Ballot ballot; + public final Mutation mutation; public final PartitionUpdate update; - public Commit(Ballot ballot, PartitionUpdate update) + public static Commit create(Ballot ballot, Commitable commitable) + { + switch (commitable.commitableKind()) + { + case MUTATION: + return new Commit(ballot, (Mutation)commitable); + case PARTITION_UPDATE: + return new Commit(ballot, (PartitionUpdate) commitable); + default: + throw new AssertionError("Unexpected commitableKind: " + commitable.commitableKind()); + } + } + + private Commit(Ballot ballot, PartitionUpdate update) { assert ballot != null; assert update != null; this.ballot = ballot; + this.mutation = new Mutation(MutationId.none(), update, PotentialTxnConflicts.ALLOW); this.update = update; } + private Commit(Ballot ballot, Mutation mutation) + { + assert ballot != null; + assert mutation != null; + assert mutation.getPartitionUpdates().size() == 1 : "Paxos commits should only have one partition update"; + + this.ballot = ballot; + this.mutation = mutation; + this.update = mutation.getOnlyUpdate(); + } + public static Commit newPrepare(DecoratedKey partitionKey, TableMetadata metadata, Ballot ballot) { return new Commit(ballot, PartitionUpdate.emptyUpdate(metadata, partitionKey)); @@ -317,15 +417,45 @@ public class Commit public Mutation makeMutation() { - // TODO (expected): what's the best thing to do here? Deriving the mutation id from the ballot seems like the best - // thing to do, like we do with the partition update timestamps, but there are caveats related to id collisions - // and the assumption that a mutation id is unique amonth other mutations, which is not the case w/ paxos ballots, - // which only need to be unique to a given partition key to be accepted. It may be best to keep them separate anyway, - // since the reconciliation process as currently planned will not allow writes with ids before some point in time, - // and there might be edge cases where that locks up paxos execution or has other side effects. The downside of - // not making paxos mutation ids deterministic is that the same commit may create multiple mutation ids if a paxos - // operation is not fully committed, then re-committed on repair or the next operation - return new Mutation(MutationId.fixme(), update, PotentialTxnConflicts.ALLOW); + return mutation; + } + + public Mutation makeMutation(MutationId mutationId) + { + if (mutationId == null || mutation.id().equals(mutationId)) + return mutation; + + PartitionUpdate update = mutation.getOnlyUpdate(); + return new Mutation(mutationId, update, mutation.potentialTxnConflicts()); + } + + /** + * Creates a copy of this Commit with a new mutation ID. + * Subclasses override to preserve their concrete type. + */ + public Commit withMutationId(MutationId mutationId) + { + return new Commit(ballot, makeMutation(mutationId)); + } + + public DecoratedKey partitionKey() + { + return update.partitionKey(); + } + + public TableMetadata metadata() + { + return update.metadata(); + } + + public EncodingStats stats() + { + return update.stats(); + } + + public boolean isEmpty() + { + return mutation.getOnlyUpdate().isEmpty(); } @Override @@ -377,7 +507,7 @@ public class Commit // the timestamp of a mutation stays unchanged as we repropose it, so the timestamp of the mutation // is the timestamp of the ballot that originally proposed it - long originalBallotOfNewer = newer.update.stats().minTimestamp; + long originalBallotOfNewer = newer.stats().minTimestamp; // so, if the mutation and ballot timestamps match, this is not a reproposal but a first proposal if (ballotOfNewer == originalBallotOfNewer) @@ -388,7 +518,7 @@ public class Commit return true; // otherwise, it could be that both are reproposals, so just check both for the "original" ballot timestamp - return originalBallotOfNewer == older.update.stats().minTimestamp; + return originalBallotOfNewer == older.stats().minTimestamp; } public CompareResult compareWith(Commit that) @@ -488,29 +618,64 @@ public class Commit public static class CommitSerializer implements IVersionedSerializer { - final BiFunction constructor; - public CommitSerializer(BiFunction constructor) + final BiFunction partitionUpdateConstructor; + final BiFunction mutationConstructor; + + public CommitSerializer(BiFunction partitionUpdateConstructor, BiFunction mutationConstructor) { - this.constructor = constructor; + this.partitionUpdateConstructor = partitionUpdateConstructor; + this.mutationConstructor = mutationConstructor; } public void serialize(T commit, DataOutputPlus out, int version) throws IOException { commit.ballot.serialize(out); - PartitionUpdate.serializer.serialize(commit.update, out, version); + + // Use version-aware serialization + if (version >= MessagingService.VERSION_61) + { + // New format: serialize Mutation directly + Mutation.serializer.serialize(commit.mutation, out, version); + } + else + { + // Legacy format: serialize PartitionUpdate + PartitionUpdate.serializer.serialize(commit.update, out, version); + } } public T deserialize(DataInputPlus in, int version) throws IOException { Ballot ballot = Ballot.deserialize(in); - PartitionUpdate update = PartitionUpdate.serializer.deserialize(in, version, DeserializationHelper.Flag.LOCAL); - return constructor.apply(ballot, update); + + if (version >= MessagingService.VERSION_61) + { + // New format: deserialize Mutation + Mutation mutation = org.apache.cassandra.db.Mutation.serializer.deserialize(in, version); + return mutationConstructor.apply(ballot, mutation); + } + else + { + // Legacy format: always PartitionUpdate + PartitionUpdate update = PartitionUpdate.serializer.deserialize(in, version, DeserializationHelper.Flag.LOCAL); + return partitionUpdateConstructor.apply(ballot, update); + } } public long serializedSize(T commit, int version) { - return Ballot.sizeInBytes() - + PartitionUpdate.serializer.serializedSize(commit.update, version); + long size = Ballot.sizeInBytes(); + + if (version >= MessagingService.VERSION_61) + { + size += Mutation.serializer.serializedSize(commit.mutation, version); + } + else + { + size += PartitionUpdate.serializer.serializedSize(commit.update, version); + } + + return size; } } diff --git a/src/java/org/apache/cassandra/service/paxos/ConsensusReadForwardHandler.java b/src/java/org/apache/cassandra/service/paxos/ConsensusReadForwardHandler.java new file mode 100644 index 0000000000..fdae1f9a1a --- /dev/null +++ b/src/java/org/apache/cassandra/service/paxos/ConsensusReadForwardHandler.java @@ -0,0 +1,122 @@ +/* + * 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.paxos; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.SinglePartitionReadCommand; +import org.apache.cassandra.db.partitions.PartitionIterator; +import org.apache.cassandra.exceptions.CassandraException; +import org.apache.cassandra.exceptions.RequestFailure; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.service.ClientWarn; +import org.apache.cassandra.service.StorageProxy; +import org.apache.cassandra.tracing.Tracing; +import org.apache.cassandra.transport.Dispatcher; + +/** + * Handler for forwarded consensus read operations. + * Executes the consensus read operation on behalf of the original coordinator, + * ensuring proper coordination for tracked keyspaces on a replica coordinator. + * + * TODO (expected): more comprehensive testing + */ +public class ConsensusReadForwardHandler implements IVerbHandler +{ + public static final ConsensusReadForwardHandler instance = new ConsensusReadForwardHandler(); + private static final Logger logger = LoggerFactory.getLogger(ConsensusReadForwardHandler.class); + + @Override + public void doVerb(Message message) + { + ConsensusReadForwardRequest request = message.payload; + SinglePartitionReadCommand command = request.command; + Tracing.trace("Executing forwarded consensus read operation for {}", command.partitionKey()); + + // Start capturing client warnings for the forwarded operation + ClientWarn.instance.captureWarnings(); + try + { + // Validate keyspace exists and is tracked + String keyspaceName = command.metadata().keyspace; + KeyspaceMetadata ksMetadata = Schema.instance.getKeyspaceMetadata(keyspaceName); + if (ksMetadata == null) + { + MessagingService.instance().respondWithFailure(RequestFailureReason.INCOMPATIBLE_SCHEMA, message); + logger.error("Failed to forward consensus read operation for non-existent keyspace {}", keyspaceName); + return; + } + + if (!ksMetadata.params.replicationType.isTracked()) + { + MessagingService.instance().respondWithFailure(RequestFailureReason.INCOMPATIBLE_SCHEMA, message); + logger.error("Asked to perform forwarded consensus read operation, but keyspace {} is not tracked", keyspaceName); + return; + } + + // Create a Group from the single command for reading + SinglePartitionReadCommand.Group group = SinglePartitionReadCommand.Group.one(command); + + // Execute the read using StorageProxy.read() which will: + // 1. Check forwarding (returns null since we're on a replica) + // 2. Execute the consensus read with the appropriate protocol + logger.debug("Executing consensus read operation for table {}.{} with key {}", + keyspaceName, command.metadata().name, command.partitionKey()); + + Dispatcher.RequestTime requestTime = Dispatcher.RequestTime.forImmediateExecution(); + PartitionIterator result = StorageProxy.readWithConsensusForwarded(group, request.consistencyLevel, requestTime); + + // Create response with the read result and captured warnings + List warnings = ClientWarn.instance.getWarnings(); + CasForwardResponse response = new CasForwardResponse(result, warnings); + MessagingService.instance().respond(response, message); + logger.debug("Completed forwarded consensus read operation for {}", command.partitionKey()); + } + catch (CassandraException ce) + { + // Forward the exception back to the original coordinator with warnings + List warnings = ClientWarn.instance.getWarnings(); + CasForwardResponse response = new CasForwardResponse(ce, warnings); + MessagingService.instance().respond(response, message); + } + catch (Throwable t) + { + try + { + MessagingService.instance().respondWithFailure(RequestFailure.forException(t), message); + } + finally + { + throw t; + } + } + finally + { + ClientWarn.instance.resetWarnings(); + } + } +} diff --git a/src/java/org/apache/cassandra/service/paxos/ConsensusReadForwardRequest.java b/src/java/org/apache/cassandra/service/paxos/ConsensusReadForwardRequest.java new file mode 100644 index 0000000000..923897e690 --- /dev/null +++ b/src/java/org/apache/cassandra/service/paxos/ConsensusReadForwardRequest.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.service.paxos; + +import java.io.IOException; + +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.db.SinglePartitionReadCommand; +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; + +/** + * Request to forward a consensus read operation to a replica coordinator. + * This is used when the original coordinator is not a replica but needs to + * execute a consensus read for a tracked keyspace that requires proper coordination. + * + * Consensus reads only ever contain a single read command. + */ +public class ConsensusReadForwardRequest +{ + public static final Serializer serializer = new Serializer(); + + public final SinglePartitionReadCommand command; + public final ConsistencyLevel consistencyLevel; + + public ConsensusReadForwardRequest(SinglePartitionReadCommand command, ConsistencyLevel consistencyLevel) + { + this.command = command; + this.consistencyLevel = consistencyLevel; + } + + public static class Serializer implements IVersionedSerializer + { + @Override + public void serialize(ConsensusReadForwardRequest forwardRequest, DataOutputPlus out, int version) throws IOException + { + ReadCommand.serializer.serialize(forwardRequest.command, out, version); + out.writeByte(forwardRequest.consistencyLevel.code); + } + + @Override + public ConsensusReadForwardRequest deserialize(DataInputPlus in, int version) throws IOException + { + ReadCommand readCommand = ReadCommand.serializer.deserialize(in, version); + if (!(readCommand instanceof SinglePartitionReadCommand)) + throw new IOException("Expected SinglePartitionReadCommand but got " + readCommand.getClass()); + ConsistencyLevel consistencyLevel = ConsistencyLevel.fromCode(in.readUnsignedByte()); + return new ConsensusReadForwardRequest((SinglePartitionReadCommand) readCommand, consistencyLevel); + } + + @Override + public long serializedSize(ConsensusReadForwardRequest forwardRequest, int version) + { + long size = ReadCommand.serializer.serializedSize(forwardRequest.command, version); + size += 1; // consistencyLevel.code + return size; + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/service/paxos/Paxos.java b/src/java/org/apache/cassandra/service/paxos/Paxos.java index a40900f73c..230944df03 100644 --- a/src/java/org/apache/cassandra/service/paxos/Paxos.java +++ b/src/java/org/apache/cassandra/service/paxos/Paxos.java @@ -21,6 +21,7 @@ package org.apache.cassandra.service.paxos; import java.io.IOException; import java.util.Collection; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -41,9 +42,13 @@ import org.slf4j.LoggerFactory; import org.apache.cassandra.config.Config; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.statements.CQL3CasRequest; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.IReadResponse; +import org.apache.cassandra.db.ReadKind; import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.ReadResponse; import org.apache.cassandra.db.SinglePartitionReadCommand; import org.apache.cassandra.db.WriteType; import org.apache.cassandra.db.partitions.FilteredPartition; @@ -53,7 +58,6 @@ import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.rows.RowIterator; import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.CasWriteTimeoutException; -import org.apache.cassandra.exceptions.ExceptionCode; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.exceptions.IsBootstrappingException; import org.apache.cassandra.exceptions.ReadFailureException; @@ -83,10 +87,10 @@ import org.apache.cassandra.locator.ReplicaLayout.ForTokenWrite; import org.apache.cassandra.locator.ReplicaPlan.ForRead; import org.apache.cassandra.metrics.ClientRequestMetrics; import org.apache.cassandra.metrics.ClientRequestSizeMetrics; +import org.apache.cassandra.net.Message; import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.service.CASRequest; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.FailureRecordingCallback.AsMap; import org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter; @@ -98,6 +102,7 @@ import org.apache.cassandra.service.paxos.cleanup.PaxosRepairState; import org.apache.cassandra.service.reads.DataResolver; import org.apache.cassandra.service.reads.ReadCoordinator; import org.apache.cassandra.service.reads.repair.NoopReadRepair; +import org.apache.cassandra.service.reads.tracked.TrackedDataResponse; import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.tcm.membership.NodeId; @@ -109,6 +114,8 @@ import org.apache.cassandra.utils.CassandraVersion; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.NoSpamLogger; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; import static java.util.Collections.emptyMap; import static java.util.concurrent.TimeUnit.NANOSECONDS; import static java.util.concurrent.TimeUnit.SECONDS; @@ -353,7 +360,7 @@ public class Paxos /** * Encapsulates the peers we will talk to for this operation. */ - static class Participants implements ForRead + public static class Participants implements ForRead { final Keyspace keyspace; @@ -499,6 +506,11 @@ public class Paxos return electorateLive.endpoint(i); } + Replica voterReplica(int i) + { + return electorateLive.get(i); + } + void assureSufficientLiveNodes(boolean isWrite) throws UnavailableException { if (sizeOfConsensusQuorum > sizeOfPoll()) @@ -635,7 +647,9 @@ public class Paxos if (isFailure) { mark(isWrite, m -> m.failures, consistency); - throw serverError != null ? new RequestFailureException(ExceptionCode.SERVER_ERROR, serverError, consistency, successes, required, failures) + throw serverError != null ? (isWrite + ? new WriteFailureException(consistency, successes, required, WriteType.CAS, failures) + : new ReadFailureException(serverError, consistency, successes, required, false, failures, null)) : isWrite ? new WriteFailureException(consistency, successes, required, WriteType.CAS, failures) : new ReadFailureException(consistency, successes, required, false, failures); @@ -706,7 +720,7 @@ public class Paxos * (since, if the CAS doesn't succeed, it means the current value do not match the conditions). */ public static ConsensusAttemptResult cas(DecoratedKey partitionKey, - CASRequest request, + CQL3CasRequest request, ConsistencyLevel consistencyForConsensus, ConsistencyLevel consistencyForCommit, ClientState clientState, @@ -752,6 +766,7 @@ public class Paxos Proposal proposal; boolean conditionMet = request.appliesTo(current); + if (!conditionMet) { if (getPaxosVariant() == v2_without_linearizable_reads_or_rejected_writes) @@ -823,7 +838,7 @@ public class Paxos // no need to commit a no-op; either it // 1) reached a majority, in which case it was agreed, had no effect and we can do nothing; or // 2) did not reach a majority, was not agreed, and was not user visible as a result so we can ignore it - if (!proposal.update.isEmpty()) + if (!proposal.isEmpty()) commit = commit(proposal.agreed(), participants, consistencyForConsensus, consistencyForCommit, true); break done; @@ -1125,15 +1140,43 @@ public class Paxos PaxosPrepare.Success success = prepare.success(); Supplier plan = () -> success.participants; - DataResolver resolver = new DataResolver<>(ReadCoordinator.DEFAULT, query, plan, NoopReadRepair.instance, requestTime); - for (int i = 0 ; i < success.responses.size() ; ++i) - resolver.preprocess(success.responses.get(i)); + List> responses = success.responses; - class WasRun implements Runnable { boolean v; public void run() { v = true; } } - WasRun hadShortRead = new WasRun(); - PartitionIterator result = resolver.resolve(hadShortRead); + // There should be only a single response from the coordinator that was selected to do the tracked read + boolean isTracked = responses.get(0).payload.kind().isTracked(); - if (!isPromised && hadShortRead.v) + PartitionIterator result = null; + boolean hadShortRead = false; + if (isTracked) + { + for (Message response : responses) + { + if (response.payload.kind() == ReadKind.TRACKED_DATA) + { + result = ((TrackedDataResponse) response.payload).makeIterator(query); + break; + } + } + checkState(result != null, "Should have found a data response"); + } + else + { + DataResolver resolver = new DataResolver<>(ReadCoordinator.DEFAULT, query, plan, NoopReadRepair.instance, requestTime); + + for (int i = 0 ; i < responses.size() ; ++i) + { + Message message = responses.get(i); + resolver.preprocess(message.withPayload((ReadResponse)message.payload)); + } + + // SERIAL supports partition range reads which can result in short reads + class WasRun implements Runnable { boolean v; public void run() { v = true; } } + WasRun hadShortReadRunnable = new WasRun(); + result = resolver.resolve(hadShortReadRunnable); + hadShortRead = hadShortReadRunnable.v; + } + + if (!isPromised && hadShortRead) { // we need to propose an empty update to linearize our short read, but only had read success // since we may continue to perform short reads, we ask our prepare not to accept an early @@ -1143,7 +1186,7 @@ public class Paxos break; } - return new BeginResult(success.ballot, success.participants, failedAttemptsDueToContention, result, !hadShortRead.v && success.isReadSafe, isPromised, success.supersededBy, false); + return new BeginResult(success.ballot, success.participants, failedAttemptsDueToContention, result, !hadShortRead && success.isReadSafe, isPromised, success.supersededBy, false); } case MAYBE_FAILURE: @@ -1170,7 +1213,7 @@ public class Paxos } } - public static boolean isInRangeAndShouldProcess(InetAddressAndPort from, DecoratedKey key, TableMetadata table, boolean includesRead) + public static boolean isInRangeAndShouldProcess(DecoratedKey key, TableMetadata table, boolean includesRead) { Keyspace keyspace = Keyspace.open(table.keyspace); // MetaStrategy distributes the entire keyspace to all replicas. In addition, its tables (currently only @@ -1324,7 +1367,7 @@ public class Paxos public static void setPaxosVariant(Config.PaxosVariant paxosVariant) { - Preconditions.checkNotNull(paxosVariant); + checkNotNull(paxosVariant); PAXOS_VARIANT = paxosVariant; DatabaseDescriptor.setPaxosVariant(paxosVariant); } diff --git a/src/java/org/apache/cassandra/service/paxos/Paxos2CommitForwardHandler.java b/src/java/org/apache/cassandra/service/paxos/Paxos2CommitForwardHandler.java new file mode 100644 index 0000000000..fac9043bfb --- /dev/null +++ b/src/java/org/apache/cassandra/service/paxos/Paxos2CommitForwardHandler.java @@ -0,0 +1,132 @@ +/* + * 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.paxos; + +import java.util.function.Consumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.exceptions.RequestFailure; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.NoPayload; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tracing.Tracing; +import org.apache.cassandra.utils.concurrent.ConditionAsConsumer; + +import static org.apache.cassandra.utils.concurrent.ConditionAsConsumer.newConditionAsConsumer; + +/** + * Handler for forwarded Paxos V2 commit requests. + * Executes the commit operation on behalf of the original coordinator, + * ensuring that MutationId generation happens on a replica coordinator. + * + * The PaxosCommit constructor handles mutation ID generation, so this handler + * simply delegates to PaxosCommit.commit() with the original commit. + * + * TODO (expected): more comprehensive testing + */ +public class Paxos2CommitForwardHandler implements IVerbHandler +{ + public static final Paxos2CommitForwardHandler instance = new Paxos2CommitForwardHandler(); + private static final Logger logger = LoggerFactory.getLogger(Paxos2CommitForwardHandler.class); + + @Override + public void doVerb(Message message) + { + // Ensure we have up-to-date cluster metadata before executing the forwarded commit + ClusterMetadataService.instance().fetchLogFromPeerOrCMS(message.from(), message.header.epoch); + Paxos2CommitForwardRequest request = message.payload; + + Tracing.trace("Executing forwarded Paxos V2 commit for {}", request.commit.partitionKey()); + + try + { + String ksName = request.commit.metadata().keyspace; + KeyspaceMetadata ksMetadata = Schema.instance.getKeyspaceMetadata(ksName); + if (ksMetadata == null) + { + MessagingService.instance().respondWithFailure(RequestFailureReason.INCOMPATIBLE_SCHEMA, message); + logger.error("Failed to forward paxos commit for non-existent keyspace {}", ksName); + return; + } + + if (!ksMetadata.params.replicationType.isTracked()) + { + MessagingService.instance().respondWithFailure(RequestFailureReason.INCOMPATIBLE_SCHEMA, message); + logger.error("Asked to perform forwarded paxos commit, but keyspace {} is not tracked", ksName); + return; + } + + // Execute the commit operation - PaxosCommit constructor handles mutation ID generation + ConditionAsConsumer onDone = newConditionAsConsumer(); + PaxosCommit.Status[] statusHolder = new PaxosCommit.Status[1]; + + Consumer statusCapture = status -> { + statusHolder[0] = status; + onDone.accept(status); + }; + + // Delegate to PaxosCommit.commit() - it will generate mutation ID in constructor + PaxosCommit.commit(request.commit, + request.all, + request.allLive, + request.allDown, + request.required, + request.isUrgent, + request.consistencyForConsensus, + request.consistencyForCommit, + false, // allowHints + statusCapture); + + // Wait for completion + try + { + onDone.awaitUntil(message.expiresAtNanos()); + PaxosCommit.Status status = statusHolder[0]; + + if (status != null && status.isSuccess()) + { + MessagingService.instance().respond(NoPayload.noPayload, message); + } + else + { + MessagingService.instance().respondWithFailure(RequestFailureReason.UNKNOWN, message); + logger.error("Forwarded Paxos V2 commit failed with status: {}", status); + } + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + MessagingService.instance().respondWithFailure(RequestFailure.forException(e), message); + logger.error("Forwarded Paxos V2 commit interrupted", e); + } + } + catch (Exception e) + { + MessagingService.instance().respondWithFailure(RequestFailure.forException(e), message); + logger.error("Failed to execute forwarded Paxos V2 commit for {}", request.commit, e); + } + } +} diff --git a/src/java/org/apache/cassandra/service/paxos/Paxos2CommitForwardRequest.java b/src/java/org/apache/cassandra/service/paxos/Paxos2CommitForwardRequest.java new file mode 100644 index 0000000000..faf8635981 --- /dev/null +++ b/src/java/org/apache/cassandra/service/paxos/Paxos2CommitForwardRequest.java @@ -0,0 +1,124 @@ +/* + * 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.paxos; + +import java.io.IOException; + +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.EndpointsForToken; +import org.apache.cassandra.service.paxos.Commit.Agreed; + +/** + * Request to forward a Paxos V2 commit operation to a replica coordinator. + * This is used when the original coordinator is not a replica but needs to + * execute a Paxos commit for a tracked keyspace that requires MutationId generation. + * + * Contains only the essential data needed by PaxosCommit instead of the full Participants object. + */ +public class Paxos2CommitForwardRequest +{ + public static final Serializer serializer = new Serializer(); + + public final Agreed commit; + public final ConsistencyLevel consistencyForConsensus; + public final ConsistencyLevel consistencyForCommit; + public final EndpointsForToken all; + public final EndpointsForToken allLive; + public final EndpointsForToken allDown; + public final int required; + public final boolean isUrgent; + + public Paxos2CommitForwardRequest(Agreed commit, + ConsistencyLevel consistencyForConsensus, + ConsistencyLevel consistencyForCommit, + EndpointsForToken all, + EndpointsForToken allLive, + EndpointsForToken allDown, + int required, + boolean isUrgent) + { + this.commit = commit; + this.consistencyForConsensus = consistencyForConsensus; + this.consistencyForCommit = consistencyForCommit; + this.all = all; + this.allLive = allLive; + this.allDown = allDown; + this.required = required; + this.isUrgent = isUrgent; + } + + public static class Serializer implements IVersionedSerializer + { + @Override + public void serialize(Paxos2CommitForwardRequest request, DataOutputPlus out, int version) throws IOException + { + Agreed.serializer.serialize(request.commit, out, version); + out.writeByte(request.consistencyForConsensus.code); + out.writeByte(request.consistencyForCommit.code); + + EndpointsForToken.serializer.serialize(request.all, out, version); + EndpointsForToken.serializer.serialize(request.allLive, out, version); + EndpointsForToken.serializer.serialize(request.allDown, out, version); + + out.writeUnsignedVInt32(request.required); + out.writeBoolean(request.isUrgent); + } + + @Override + public Paxos2CommitForwardRequest deserialize(DataInputPlus in, int version) throws IOException + { + Agreed commit = Agreed.serializer.deserialize(in, version); + ConsistencyLevel consistencyForConsensus = ConsistencyLevel.fromCode(in.readUnsignedByte()); + ConsistencyLevel consistencyForCommit = ConsistencyLevel.fromCode(in.readUnsignedByte()); + + IPartitioner partitioner = commit.metadata().partitioner; + EndpointsForToken all = EndpointsForToken.serializer.deserialize(in, partitioner, version); + EndpointsForToken allLive = EndpointsForToken.serializer.deserialize(in, partitioner, version); + EndpointsForToken allDown = EndpointsForToken.serializer.deserialize(in, partitioner, version); + + int required = in.readUnsignedVInt32(); + boolean isUrgent = in.readBoolean(); + + return new Paxos2CommitForwardRequest(commit, consistencyForConsensus, consistencyForCommit, + all, allLive, allDown, required, isUrgent); + } + + @Override + public long serializedSize(Paxos2CommitForwardRequest request, int version) + { + long size = Agreed.serializer.serializedSize(request.commit, version) + + 1 // consistencyForConsensus.code + + 1; // consistencyForCommit.code + + size += EndpointsForToken.serializer.serializedSize(request.all, version); + size += EndpointsForToken.serializer.serializedSize(request.allLive, version); + size += EndpointsForToken.serializer.serializedSize(request.allDown, version); + + size += TypeSizes.sizeofUnsignedVInt(request.required); + size += TypeSizes.BOOL_SIZE; // isUrgent + + return size; + } + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/service/paxos/PaxosCommit.java b/src/java/org/apache/cassandra/service/paxos/PaxosCommit.java index b79e032fe2..41e85cf105 100644 --- a/src/java/org/apache/cassandra/service/paxos/PaxosCommit.java +++ b/src/java/org/apache/cassandra/service/paxos/PaxosCommit.java @@ -21,14 +21,18 @@ package org.apache.cassandra.service.paxos; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import java.util.function.Consumer; +import javax.annotation.Nullable; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.agrona.collections.IntHashSet; import org.apache.cassandra.concurrent.ExecutorPlus; import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.Mutation; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.RequestFailure; import org.apache.cassandra.locator.EndpointsForToken; import org.apache.cassandra.locator.InOurDc; @@ -39,10 +43,17 @@ import org.apache.cassandra.net.IVerbHandler; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.net.NoPayload; +import org.apache.cassandra.net.RequestCallback; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.replication.MutationId; +import org.apache.cassandra.replication.MutationTrackingService; import org.apache.cassandra.service.paxos.Paxos.Participants; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.tracing.Tracing; +import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.concurrent.ConditionAsConsumer; +import static com.google.common.base.Preconditions.checkState; import static java.util.Collections.emptyMap; import static org.apache.cassandra.exceptions.RequestFailureReason.UNKNOWN; import static org.apache.cassandra.net.Verb.PAXOS2_COMMIT_REMOTE_REQ; @@ -102,6 +113,9 @@ public class PaxosCommit> ex final int required; final OnDone onDone; + @Nullable + final IntHashSet remoteReplicas; + /** * packs two 32-bit integers; * bit 00-31: accepts @@ -112,15 +126,52 @@ public class PaxosCommit> ex */ private volatile long responses; - public PaxosCommit(Agreed commit, boolean allowHints, ConsistencyLevel consistencyForConsensus, ConsistencyLevel consistencyForCommit, Participants participants, OnDone onDone) + public PaxosCommit(Agreed commit, boolean allowHints, ConsistencyLevel consistencyForConsensus, ConsistencyLevel consistencyForCommit, EndpointsForToken replicas, int required, OnDone onDone) { - this.commit = commit; + // Check if this is a tracked keyspace + boolean isTracked = commit.metadata().replicationType().isTracked(); + + Agreed commitToUse = commit; + IntHashSet remoteReplicas = null; + if (isTracked) + { + // Precondition: for tracked keyspaces, the local node must be a replica + // so it can generate an ID + InetAddressAndPort localEndpoint = FBUtilities.getBroadcastAddressAndPort(); + checkState(replicas.endpoints().contains(localEndpoint), + "For tracked keyspaces, the coordinator must be a replica. Local endpoint %s not in replicas %s", + localEndpoint, replicas.endpoints()); + + // Generate mutation ID if the commit doesn't already have one + // (commits loaded from system.paxos may already have the saved mutation ID) + if (commit.mutation.id().isNone()) + { + Token token = commit.partitionKey().getToken(); + MutationId mutationId = MutationTrackingService.instance.nextMutationId(commit.metadata().keyspace, token); + Mutation mutationWithId = commit.makeMutation(mutationId); + commitToUse = new Commit.Agreed(commit.ballot, mutationWithId); + } + + // Collect remote replicas for tracking service + remoteReplicas = new IntHashSet(); + ClusterMetadata metadata = ClusterMetadata.current(); + for (int i = 0; i < replicas.size(); i++) + { + Replica replica = replicas.get(i); + if (!replica.isSelf()) + remoteReplicas.add(metadata.directory.peerId(replica.endpoint()).id()); + } + } + + this.commit = commitToUse; this.allowHints = allowHints; this.consistencyForConsensus = consistencyForConsensus; this.consistencyForCommit = consistencyForCommit; - this.replicas = participants.all; + this.replicas = replicas; this.onDone = onDone; - this.required = participants.requiredFor(consistencyForCommit); + this.required = required; + this.remoteReplicas = remoteReplicas; + if (required == 0) onDone.accept(status()); } @@ -128,14 +179,45 @@ public class PaxosCommit> ex /** * Submit the proposal for commit with all replicas, and wait synchronously until at most {@code deadline} for the result */ - static Paxos.Async commit(Agreed commit, Participants participants, ConsistencyLevel consistencyForConsensus, ConsistencyLevel consistencyForCommit, /** @deprecated See CASSANDRA-17164 */ @Deprecated(since = "4.1") boolean allowHints) + static Paxos.Async commit(Agreed commit, EndpointsForToken all, EndpointsForToken allLive, EndpointsForToken allDown, int required, boolean isUrgent, ConsistencyLevel consistencyForConsensus, ConsistencyLevel consistencyForCommit, /** @deprecated See CASSANDRA-17164 */ @Deprecated(since = "4.1") boolean allowHints) { + // Check if this is a tracked keyspace requiring forwarding to a replica coordinator + if (isTrackedKeyspaceRequiringForwarding(commit, all)) + { + // For async version, create a wrapper that handles forwarding + Status[] statusHolder = new Status[1]; + ConditionAsConsumer condition = newConditionAsConsumer(); + Consumer statusCapture = status -> { + statusHolder[0] = status; + condition.accept(status); + }; + forwardPaxos2Commit(commit, all, allLive, allDown, required, isUrgent, consistencyForConsensus, consistencyForCommit, statusCapture); + + return new Paxos.Async() + { + @Override + public Status awaitUntil(long deadline) + { + try + { + condition.awaitUntil(deadline); + return statusHolder[0] != null ? statusHolder[0] : new Status(new Paxos.MaybeFailure(true, all.size(), required, 0, emptyMap())); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + return new Status(new Paxos.MaybeFailure(true, all.size(), required, 0, emptyMap())); + } + } + }; + } + // to avoid unnecessary object allocations we extend PaxosPropose to implements Paxos.Async class Async extends PaxosCommit> implements Paxos.Async { - private Async(Agreed commit, boolean allowHints, ConsistencyLevel consistencyForConsensus, ConsistencyLevel consistencyForCommit, Participants participants) + private Async(Agreed commit, boolean allowHints, ConsistencyLevel consistencyForConsensus, ConsistencyLevel consistencyForCommit, EndpointsForToken all, int required) { - super(commit, allowHints, consistencyForConsensus, consistencyForCommit, participants, newConditionAsConsumer()); + super(commit, allowHints, consistencyForConsensus, consistencyForCommit, all, required, newConditionAsConsumer()); } public Status awaitUntil(long deadline) @@ -154,38 +236,126 @@ public class PaxosCommit> ex } } - Async async = new Async(commit, allowHints, consistencyForConsensus, consistencyForCommit, participants); - async.start(participants, false); + Async async = new Async(commit, allowHints, consistencyForConsensus, consistencyForCommit, all, required); + async.start(allLive, allDown, isUrgent, false); return async; } /** * Submit the proposal for commit with all replicas, and wait synchronously until at most {@code deadline} for the result */ + static > T commit(Agreed commit, EndpointsForToken all, EndpointsForToken allLive, EndpointsForToken allDown, int required, boolean isUrgent, ConsistencyLevel consistencyForConsensus, ConsistencyLevel consistencyForCommit, /** @deprecated See CASSANDRA-17164 */ @Deprecated(since = "4.1") boolean allowHints, T onDone) + { + // Check if this is a tracked keyspace requiring forwarding to a replica coordinator + if (isTrackedKeyspaceRequiringForwarding(commit, all)) + { + forwardPaxos2Commit(commit, all, allLive, allDown, required, isUrgent, consistencyForConsensus, consistencyForCommit, onDone); + return onDone; + } + + new PaxosCommit<>(commit, allowHints, consistencyForConsensus, consistencyForCommit, all, required, onDone) + .start(allLive, allDown, isUrgent, true); + return onDone; + } + + static Paxos.Async commit(Agreed commit, Participants participants, ConsistencyLevel consistencyForConsensus, ConsistencyLevel consistencyForCommit, /** @deprecated See CASSANDRA-17164 */ @Deprecated(since = "4.1") boolean allowHints) + { + return commit(commit, participants.all, participants.allLive, participants.allDown, + participants.requiredFor(consistencyForCommit), participants.isUrgent(), + consistencyForConsensus, consistencyForCommit, allowHints); + } + static > T commit(Agreed commit, Participants participants, ConsistencyLevel consistencyForConsensus, ConsistencyLevel consistencyForCommit, /** @deprecated See CASSANDRA-17164 */ @Deprecated(since = "4.1") boolean allowHints, T onDone) { - new PaxosCommit<>(commit, allowHints, consistencyForConsensus, consistencyForCommit, participants, onDone) - .start(participants, true); - return onDone; + return commit(commit, participants.all, participants.allLive, participants.allDown, + participants.requiredFor(consistencyForCommit), participants.isUrgent(), + consistencyForConsensus, consistencyForCommit, allowHints, onDone); } /** * Send commit messages to peers (or self) */ - void start(Participants participants, boolean async) + void start(EndpointsForToken allLive, EndpointsForToken allDown, boolean isUrgent, boolean async) { - boolean executeOnSelf = false; - Message commitMessage = Message.out(PAXOS_COMMIT_REQ, commit, participants.isUrgent()); + Message commitMessage = Message.out(PAXOS_COMMIT_REQ, commit, isUrgent); Message mutationMessage = null; if (ENABLE_DC_LOCAL_COMMIT && consistencyForConsensus.isDatacenterLocal()) - mutationMessage = Message.out(PAXOS2_COMMIT_REMOTE_REQ, commit.makeMutation(), participants.isUrgent()); + mutationMessage = Message.out(PAXOS2_COMMIT_REMOTE_REQ, commit.makeMutation(), isUrgent); - for (int i = 0, mi = participants.allLive.size(); i < mi ; ++i) - executeOnSelf |= isSelfOrSend(commitMessage, mutationMessage, participants.allLive.endpoint(i)); + // For tracked keyspaces, the local commit MUST execute synchronously BEFORE sending to remote replicas. + // This ensures the mutation is written to the journal before any failure callback can trigger + // reconciliation via ActiveLogReconciler. Without this ordering, a fast remote failure could schedule + // reconciliation for a mutation that hasn't been journaled yet, causing NullPointerException. + boolean isTrackedKeyspace = remoteReplicas != null; + boolean localExecutedSynchronously = false; + InetAddressAndPort localEndpoint = FBUtilities.getBroadcastAddressAndPort(); - for (int i = 0, mi = participants.allDown.size(); i < mi ; ++i) - onFailure(participants.allDown.endpoint(i), RequestFailure.NODE_DOWN); + if (isTrackedKeyspace) + { + // For tracked keyspaces, we MUST execute locally synchronously, regardless of USE_SELF_EXECUTION setting. + // This is critical because retries are scheduled on the local ActiveLogReconciler and look up mutations + // in the local MutationJournal. If we don't execute synchronously first, a fast remote failure could + // trigger a retry before the mutation that hasn't been journaled yet, causing NullPointerException. + // + // We check BOTH allLive AND allDown because the local endpoint might be incorrectly considered DOWN + // (e.g., in simulation or during network partition recovery). If local is in allDown, we STILL need + // to execute locally to write to the journal, but we'll also skip calling onFailure for self below. + boolean localIsReplica = false; + for (int i = 0, mi = allLive.size(); i < mi; ++i) + { + if (allLive.endpoint(i).equals(localEndpoint)) + { + localIsReplica = true; + break; + } + } + if (!localIsReplica) + { + for (int i = 0, mi = allDown.size(); i < mi; ++i) + { + if (allDown.endpoint(i).equals(localEndpoint)) + { + localIsReplica = true; + break; + } + } + } + if (localIsReplica) + { + executeOnSelf(); + localExecutedSynchronously = true; + } + } + + // Now send to remote replicas (and record local execution for non-tracked keyspaces) + boolean executeOnSelf = false; + for (int i = 0, mi = allLive.size(); i < mi ; ++i) + { + InetAddressAndPort endpoint = allLive.endpoint(i); + // Skip self if we already executed synchronously for tracked keyspace. + // Use direct comparison instead of shouldExecuteOnSelf to avoid dependence on USE_SELF_EXECUTION. + if (localExecutedSynchronously && endpoint.equals(localEndpoint)) + continue; + executeOnSelf |= isSelfOrSend(commitMessage, mutationMessage, endpoint); + } + + for (int i = 0, mi = allDown.size(); i < mi ; ++i) + { + InetAddressAndPort endpoint = allDown.endpoint(i); + // Skip self if we already executed synchronously for tracked keyspace. + // We can't "retry" to self via network anyway, and we've already written to the journal. + if (localExecutedSynchronously && endpoint.equals(localEndpoint)) + continue; + onFailure(endpoint, RequestFailure.NODE_DOWN); + } + + // Tracked if remoteReplicas != null, register write request with tracking service for tracked keyspaces + if (remoteReplicas != null) + { + checkState(!remoteReplicas.isEmpty()); + MutationTrackingService.instance.sentWriteRequest(commit.makeMutation(), remoteReplicas); + } if (executeOnSelf) { @@ -218,6 +388,11 @@ public class PaxosCommit> ex return locator.local().sameDatacenter(locator.location(destination)); } + private boolean isTracked() + { + return !commit.mutation.id().equals(MutationId.none()); + } + /** * Record a failure or timeout, and maybe submit a hint to {@code from} */ @@ -227,6 +402,10 @@ public class PaxosCommit> ex if (logger.isTraceEnabled()) logger.trace("{} {} from {}", commit, reason, from); + // Track failed response for tracked keyspaces + if (isTracked()) + MutationTrackingService.instance.retryFailedWrite(commit.mutation.id(), from, reason); + response(false, from); Replica replica = replicas.lookup(from); @@ -241,6 +420,11 @@ public class PaxosCommit> ex { logger.trace("{} Success from {}", commit, response.from()); + // Track successful response for tracked keyspaces + // (Local mutations are witnessed from Keyspace.applyInternalTracked) + if (isTracked()) + MutationTrackingService.instance.receivedWriteResponse(commit.mutation.id(), response.from()); + response(true, response.from()); } @@ -249,12 +433,40 @@ public class PaxosCommit> ex */ public void executeOnSelf() { - executeOnSelf(commit, RequestHandler::execute); + if (isTracked()) + { + // For tracked keyspaces, local execution MUST succeed and write to journal. + // Use direct execution instead of executeOnSelf to ensure we detect failures. + NoPayload response = RequestHandler.execute(commit); + if (response == null) + { + throw new IllegalStateException(String.format( + "Local execution failed for tracked mutation %s. " + + "isInRangeAndShouldProcess returned false but this node is the coordinator for a tracked keyspace. " + + "partitionKey=%s, table=%s, localEndpoint=%s", + commit.mutation.id(), commit.partitionKey(), commit.metadata().keyspace + "." + commit.metadata().name, + FBUtilities.getBroadcastAddressAndPort())); + } + onResponse(response, FBUtilities.getBroadcastAddressAndPort()); + } + else + { + executeOnSelf(commit, RequestHandler::execute); + } } @Override public void onResponse(NoPayload response, InetAddressAndPort from) { + // Track successful response for tracked keyspaces + if (isTracked()) + { + if (response != null) + MutationTrackingService.instance.receivedWriteResponse(commit.mutation.id(), from); + else + MutationTrackingService.instance.retryFailedWrite(commit.mutation.id(), from, RequestFailure.UNKNOWN); + } + response(response != null, from); } @@ -307,7 +519,7 @@ public class PaxosCommit> ex @Override public void doVerb(Message message) { - NoPayload response = execute(message.payload, message.from()); + NoPayload response = execute(message.payload); // NOTE: for correctness, this must be our last action, so that we cannot throw an error and send both a response and a failure response if (response == null) MessagingService.instance().respondWithFailure(UNKNOWN, message); @@ -315,15 +527,103 @@ public class PaxosCommit> ex MessagingService.instance().respond(response, message); } - private static NoPayload execute(Agreed agreed, InetAddressAndPort from) + private static NoPayload execute(Agreed agreed) { - if (!Paxos.isInRangeAndShouldProcess(from, agreed.update.partitionKey(), agreed.update.metadata(), false)) + if (!Paxos.isInRangeAndShouldProcess(agreed.partitionKey(), agreed.metadata(), false)) return null; PaxosState.commitDirect(agreed); - Tracing.trace("Enqueuing acknowledge to {}", from); + Tracing.trace("Enqueuing acknowledge to {}", agreed.ballot); return NoPayload.noPayload; } } + /** + * Checks if this commit needs to be forwarded to a replica coordinator for tracked keyspace support. + */ + private static boolean isTrackedKeyspaceRequiringForwarding(Agreed commit, EndpointsForToken all) + { + if (!commit.metadata().replicationType().isTracked()) + return false; + + // Check if current coordinator is not a replica + InetAddressAndPort localEndpoint = FBUtilities.getBroadcastAddressAndPort(); + boolean isLocalReplica = all.endpoints().contains(localEndpoint); + return !isLocalReplica; + } + + /** + * Forwards a Paxos V2 commit operation to a replica coordinator for tracked keyspaces. + */ + private static > void forwardPaxos2Commit(Agreed commit, + EndpointsForToken all, + EndpointsForToken allLive, + EndpointsForToken allDown, + int required, + boolean isUrgent, + ConsistencyLevel consistencyForConsensus, + ConsistencyLevel consistencyForCommit, + T onDone) + { + InetAddressAndPort localEndpoint = FBUtilities.getBroadcastAddressAndPort(); + + // Filter out local endpoint and sort by proximity to find best replica to forward to + EndpointsForToken liveReplicasExcludingSelf = allLive.filter(r -> !r.endpoint().equals(localEndpoint)); + + if (liveReplicasExcludingSelf.isEmpty()) + { + // No live replica available to forward to + logger.debug("No live replicas available to forward Paxos V2 commit for {}", commit.partitionKey()); + Tracing.trace("No live replicas available to forward Paxos V2 commit"); + onDone.accept(new Status(new Paxos.MaybeFailure(true, 1, 1, 0, emptyMap()))); + return; + } + + // Sort by proximity and select the best coordinator + EndpointsForToken sortedReplicas = DatabaseDescriptor.getNodeProximity().sortedByProximity(localEndpoint, liveReplicasExcludingSelf); + InetAddressAndPort replicaCoordinator = sortedReplicas.get(0).endpoint(); + + logger.debug("Forwarding Paxos V2 commit for {} to replica coordinator {}", commit.partitionKey(), replicaCoordinator); + Tracing.trace("Forwarding Paxos V2 commit to replica coordinator {}", replicaCoordinator); + + // Create forward request with extracted participant data + Paxos2CommitForwardRequest forwardRequest = new Paxos2CommitForwardRequest(commit, consistencyForConsensus, consistencyForCommit, + all, allLive, allDown, + required, isUrgent); + Message message = Message.out(Verb.PAXOS2_COMMIT_FORWARD_REQ, forwardRequest); + + // Create callback to handle forwarding response + RequestCallback callback = new RequestCallback() + { + @Override + public void onResponse(Message response) + { + Tracing.trace("Forwarded Paxos V2 commit completed successfully"); + onDone.accept(success); + } + + @Override + public void onFailure(InetAddressAndPort from, RequestFailure failure) + { + logger.debug("Forwarded Paxos V2 commit to {} failed: {}", from, failure); + Tracing.trace("Forwarded Paxos V2 commit to {} failed: {}", from, failure); + // Populate the failure map with the actual failure reason; contacted=1, required=1 for forwarded request + onDone.accept(new Status(new Paxos.MaybeFailure(true, 1, 1, 0, + java.util.Collections.singletonMap(from, failure.reason)))); + } + }; + + try + { + MessagingService.instance().sendWithCallback(message, replicaCoordinator, callback); + } + catch (Exception e) + { + logger.debug("Failed to send forwarded Paxos V2 commit to {}: {}", replicaCoordinator, e.getMessage()); + Tracing.trace("Failed to send forwarded Paxos V2 commit: {}", e.getMessage()); + onDone.accept(new Status(new Paxos.MaybeFailure(true, 1, 1, 0, + java.util.Collections.singletonMap(replicaCoordinator, UNKNOWN)))); + } + } + } diff --git a/src/java/org/apache/cassandra/service/paxos/PaxosCommitAndPrepare.java b/src/java/org/apache/cassandra/service/paxos/PaxosCommitAndPrepare.java index 41546d795a..0cd16102ab 100644 --- a/src/java/org/apache/cassandra/service/paxos/PaxosCommitAndPrepare.java +++ b/src/java/org/apache/cassandra/service/paxos/PaxosCommitAndPrepare.java @@ -20,11 +20,12 @@ package org.apache.cassandra.service.paxos; import java.io.IOException; +import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.EmbeddableSinglePartitionReadCommand; import org.apache.cassandra.db.SinglePartitionReadCommand; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; -import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.IVerbHandler; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; @@ -34,8 +35,15 @@ import org.apache.cassandra.service.paxos.Commit.Agreed; import org.apache.cassandra.service.paxos.PaxosPrepare.Rejected; import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.service.paxos.PaxosPrepare.Response; +import org.apache.cassandra.service.reads.tracked.TrackedRead; +import org.apache.cassandra.service.reads.tracked.TrackedRead.Id; import org.apache.cassandra.tracing.Tracing; +import org.apache.cassandra.transport.Dispatcher.RequestTime; +import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.concurrent.ImmediateFuture; +import static com.google.common.util.concurrent.Futures.getUnchecked; import static org.apache.cassandra.exceptions.RequestFailureReason.UNKNOWN; import static org.apache.cassandra.net.Verb.PAXOS2_COMMIT_AND_PREPARE_REQ; import static org.apache.cassandra.service.consensus.migration.ConsensusKeyMigrationState.getKeyMigrationState; @@ -50,21 +58,44 @@ public class PaxosCommitAndPrepare static PaxosPrepare commitAndPrepare(Agreed commit, Paxos.Participants participants, SinglePartitionReadCommand readCommand, boolean isWrite, boolean acceptEarlyReadSuccess) { Ballot ballot = newBallot(commit.ballot, participants.consistencyForConsensus); - Request request = new Request(commit, ballot, participants.electorate, readCommand, isWrite, true); - PaxosPrepare prepare = new PaxosPrepare(participants, request, acceptEarlyReadSuccess, null); Tracing.trace("Committing {}; Preparing {}", commit.ballot, ballot); - Message message = Message.out(PAXOS2_COMMIT_AND_PREPARE_REQ, request, participants.isUrgent()); + /* + * For simplicity with tracked keyspaces do the commit as a regular commit synchronously and then separately do a regular prepare. + * CommitAndPrepare goes down the prepare path with a message containing the commit along with the prepare + * which means this node is the coordinator and would need to either re-use the original commit mutation id + * (which wasn't saved in the system table) or generate a new one which it might not be able to do without forwarding. + * + * All these things are tractable to do better, but for now doing something simple and correct. + */ + if (readCommand.metadata().replicationType().isTracked()) + { + /* + * Consistency for consensus is tricky to pick here. The goal of sending this commit is to unblock the prepare + * on nodes that are missing the commit. CommitAndPrepare is an outcome that occurs when prepare/propose already failed + * because enough nodes were missing a commmit so we need to try again. To keep things highly available we + * use the same consistency as consensus so that when we go to do the prepare there are enough nodes + * we know have the commit that this can succeed. + */ + PaxosCommit.commit(commit, participants, participants.consistencyForConsensus, participants.consistencyForConsensus, isWrite); + return PaxosPrepare.prepareWithBallot(ballot, participants, readCommand, isWrite, acceptEarlyReadSuccess); + } + else + { + Request request = new Request(commit, ballot, participants.electorate, readCommand, isWrite, true); + PaxosPrepare prepare = new PaxosPrepare(participants, request, acceptEarlyReadSuccess, null); + Message message = Message.out(PAXOS2_COMMIT_AND_PREPARE_REQ, request, participants.isUrgent()); - start(prepare, participants, message, RequestHandler::execute); - return prepare; + start(prepare, participants, message, RequestHandler::execute); + return prepare; + } } private static class Request extends PaxosPrepare.AbstractRequest { final Agreed commit; - Request(Agreed commit, Ballot ballot, Paxos.Electorate electorate, SinglePartitionReadCommand read, boolean isWrite, boolean isForRecovery) + Request(Agreed commit, Ballot ballot, Paxos.Electorate electorate, EmbeddableSinglePartitionReadCommand read, boolean isWrite, boolean isForRecovery) { super(ballot, electorate, read, isWrite, isForRecovery); this.commit = commit; @@ -81,6 +112,18 @@ public class PaxosCommitAndPrepare return new Request(commit, ballot, electorate, partitionKey, table, isForWrite, isForRecovery); } + @Override + public Request asTrackedDataRequest(Id id, ConsistencyLevel consistencyLevel, int dataNode, int[] summaryNodes) + { + return new Request(commit, ballot, electorate, new TrackedRead.DataRequest(id, (SinglePartitionReadCommand)read, dataNode, summaryNodes, consistencyLevel), isForWrite, isForRecovery); + } + + @Override + public Request asTrackedSummaryRequest(Id id, int dataNode, int[] summaryNodes) + { + return new Request(commit, ballot, electorate, new TrackedRead.SummaryRequest(id, (SinglePartitionReadCommand)read, dataNode, summaryNodes), isForWrite, isForRecovery); + } + public String toString() { return commit.toString("CommitAndPrepare(") + ", " + Ballot.toString(ballot) + ')'; @@ -89,7 +132,7 @@ public class PaxosCommitAndPrepare public static class RequestSerializer extends PaxosPrepare.AbstractRequestSerializer { - Request construct(Agreed param, Ballot ballot, Paxos.Electorate electorate, SinglePartitionReadCommand read, boolean isWrite, boolean isForRecovery) + Request construct(Agreed param, Ballot ballot, Paxos.Electorate electorate, EmbeddableSinglePartitionReadCommand read, boolean isWrite, boolean isForRecovery) { return new Request(param, ballot, electorate, read, isWrite, isForRecovery); } @@ -128,32 +171,33 @@ public class PaxosCommitAndPrepare { ClusterMetadataService.instance().fetchLogFromPeerOrCMS(ClusterMetadata.current(), message.from(), message.epoch()); - PaxosPrepare.Response response = execute(message.payload, message.from()); + Future response = execute(message.payload, new RequestTime(message.createdAtNanos())); if (response == null) MessagingService.instance().respondWithFailure(UNKNOWN, message); else - MessagingService.instance().respond(response, message); + // TODO unwrap error for error handling in the verb + MessagingService.instance().respond(getUnchecked(response), message); } - private static PaxosPrepare.Response execute(Request request, InetAddressAndPort from) + private static Future execute(Request request, RequestTime requestTime) { Agreed commit = request.commit; - if (!Paxos.isInRangeAndShouldProcess(from, commit.update.partitionKey(), commit.update.metadata(), request.read != null)) + if (!Paxos.isInRangeAndShouldProcess(commit.partitionKey(), commit.metadata(), request.read != null)) return null; // This can be done outside the lock ClusterMetadata cm = ClusterMetadata.current(); - KeyMigrationState keyMigrationState = getKeyMigrationState(cm, commit.update.metadata().id, commit.update.partitionKey()); + KeyMigrationState keyMigrationState = getKeyMigrationState(cm, commit.metadata().id, commit.partitionKey()); // Make sure the operation is safe and there is no Accord state that needs application // Also need to know max HLC in order to accept this ballot long maxHLC = keyMigrationState.maybePerformAccordToPaxosKeyMigration(true); if (maxHLC >= commit.ballot.unixMicros()) - return new Rejected(Ballot.atUnixMicrosWithLsb(maxHLC + 1, 0, commit.ballot.flag())); + return ImmediateFuture.success(new Rejected(Ballot.atUnixMicrosWithLsb(maxHLC + 1, 0, commit.ballot.flag()))); try (PaxosState state = PaxosState.get(commit)) { state.commit(commit); - return PaxosPrepare.RequestHandler.execute(request, state, cm); + return PaxosPrepare.RequestHandler.execute(requestTime, request, state, cm); } } } diff --git a/src/java/org/apache/cassandra/service/paxos/PaxosCommitForwardHandler.java b/src/java/org/apache/cassandra/service/paxos/PaxosCommitForwardHandler.java new file mode 100644 index 0000000000..728b9b78a5 --- /dev/null +++ b/src/java/org/apache/cassandra/service/paxos/PaxosCommitForwardHandler.java @@ -0,0 +1,89 @@ +/* + * 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.paxos; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.exceptions.RequestFailure; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.NoPayload; +import org.apache.cassandra.service.StorageProxy; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tracing.Tracing; +import org.apache.cassandra.transport.Dispatcher; + +/** + * Handler for forwarded Paxos V1 commit requests. + * Executes the commit operation on behalf of the original coordinator, + * ensuring that MutationId generation happens on a replica coordinator. + * + * TODO (expected): more comprehensive testing + */ +public class PaxosCommitForwardHandler implements IVerbHandler +{ + public static final PaxosCommitForwardHandler instance = new PaxosCommitForwardHandler(); + private static final Logger logger = LoggerFactory.getLogger(PaxosCommitForwardHandler.class); + + @Override + public void doVerb(Message message) + { + // PaxosV1 when doing commit picks whatever the current replicas are to send the commits to + // so make sure we at least match what they would have picked + ClusterMetadataService.instance().fetchLogFromPeerOrCMS(message.from(), message.header.epoch); + PaxosCommitForwardRequest request = message.payload; + + Tracing.trace("Executing forwarded Paxos commit for {}", request.proposal.partitionKey()); + + try + { + String ksName = request.proposal.metadata().keyspace; + Keyspace keyspace = Keyspace.openIfExists(ksName); + if (keyspace == null) + { + MessagingService.instance().respondWithFailure(RequestFailureReason.INCOMPATIBLE_SCHEMA, message); + logger.error("Failed to forward paxos commit for non-existent keyspace {}", ksName); + return; + } + + if (!keyspace.getMetadata().params.replicationType.isTracked()) + { + MessagingService.instance().respondWithFailure(RequestFailureReason.INCOMPATIBLE_SCHEMA, message); + logger.error("Asked to perform forwarded paxos commit, but keyspace {} is not tracked", ksName); + return; + } + + // Call commitPaxosTracked which handles mutation ID generation, sending to all replicas, + // and tracking. The respondAfterSend flag determines if we wait for application. + StorageProxy.commitPaxosTracked(keyspace, request.proposal, request.consistencyLevel, + Dispatcher.RequestTime.forImmediateExecution(), request.respondAfterSend); + + MessagingService.instance().respond(NoPayload.noPayload, message); + } + catch (Exception e) + { + MessagingService.instance().respondWithFailure(RequestFailure.forException(e), message); + logger.error("Failed to execute forwarded paxos commit for {}", request.proposal, e); + } + } +} diff --git a/src/java/org/apache/cassandra/service/paxos/PaxosCommitForwardRequest.java b/src/java/org/apache/cassandra/service/paxos/PaxosCommitForwardRequest.java new file mode 100644 index 0000000000..ee70ddd1d4 --- /dev/null +++ b/src/java/org/apache/cassandra/service/paxos/PaxosCommitForwardRequest.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.cassandra.service.paxos; + +import java.io.IOException; + +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; + +/** + * Request to forward a Paxos V1 commit operation to a replica coordinator. + * This is used when the original coordinator is not a replica but needs to + * execute a Paxos commit for a tracked keyspace that requires MutationId generation. + * + * When {@code respondAfterSend} is true, the handler will respond immediately after + * sending the commits to replicas without waiting for application. This is used + * for the sendCommit path where we need to ensure commits were dispatched before + * continuing with a prepare operation. + */ +public class PaxosCommitForwardRequest +{ + public static final Serializer serializer = new Serializer(); + + public final Commit proposal; + public final ConsistencyLevel consistencyLevel; + /** + * If true, the handler will respond immediately after sending the commits + * to replicas without waiting for application to complete. + */ + public final boolean respondAfterSend; + + public PaxosCommitForwardRequest(Commit proposal, ConsistencyLevel consistencyLevel) + { + this(proposal, consistencyLevel, false); + } + + public PaxosCommitForwardRequest(Commit proposal, ConsistencyLevel consistencyLevel, boolean respondAfterSend) + { + this.proposal = proposal; + this.consistencyLevel = consistencyLevel; + this.respondAfterSend = respondAfterSend; + } + + public static class Serializer implements IVersionedSerializer + { + @Override + public void serialize(PaxosCommitForwardRequest request, DataOutputPlus out, int version) throws IOException + { + Commit.serializer.serialize(request.proposal, out, version); + out.write(request.consistencyLevel.code); + out.writeBoolean(request.respondAfterSend); + } + + @Override + public PaxosCommitForwardRequest deserialize(DataInputPlus in, int version) throws IOException + { + Commit proposal = Commit.serializer.deserialize(in, version); + ConsistencyLevel consistencyLevel = ConsistencyLevel.fromCode(in.readUnsignedByte()); + boolean respondAfterSend = in.readBoolean(); + return new PaxosCommitForwardRequest(proposal, consistencyLevel, respondAfterSend); + } + + @Override + public long serializedSize(PaxosCommitForwardRequest request, int version) + { + return Commit.serializer.serializedSize(request.proposal, version) + + 1 // consistencyLevel + + 1; // respondAfterSend + } + } +} diff --git a/src/java/org/apache/cassandra/service/paxos/PaxosPrepare.java b/src/java/org/apache/cassandra/service/paxos/PaxosPrepare.java index f7304ba49e..9b131697ab 100644 --- a/src/java/org/apache/cassandra/service/paxos/PaxosPrepare.java +++ b/src/java/org/apache/cassandra/service/paxos/PaxosPrepare.java @@ -36,9 +36,11 @@ import org.slf4j.LoggerFactory; 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.IReadResponse; +import org.apache.cassandra.db.EmbeddableSinglePartitionReadCommand; import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.db.ReadCommand; import org.apache.cassandra.db.ReadExecutionController; import org.apache.cassandra.db.ReadResponse; import org.apache.cassandra.db.SinglePartitionReadCommand; @@ -54,6 +56,12 @@ import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.service.reads.tracked.TrackedRead; +import org.apache.cassandra.service.reads.tracked.TrackedRead.DataRequest; +import org.apache.cassandra.service.reads.tracked.TrackedRead.Id; +import org.apache.cassandra.service.reads.tracked.TrackedRead.SummaryRequest; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.metrics.PaxosMetrics; import org.apache.cassandra.net.IVerbHandler; import org.apache.cassandra.net.Message; @@ -64,14 +72,20 @@ import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.consensus.migration.ConsensusKeyMigrationState.KeyMigrationState; import org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter; import org.apache.cassandra.service.paxos.PaxosPrepare.Status.Outcome; -import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.tcm.ClusterMetadataService; import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.tracing.Tracing; +import org.apache.cassandra.transport.Dispatcher.RequestTime; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.concurrent.ImmediateFuture; import org.apache.cassandra.utils.vint.VIntCoding; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.util.concurrent.Futures.getUnchecked; import static java.util.Collections.emptyMap; import static org.apache.cassandra.exceptions.RequestFailureReason.RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM; +import static org.apache.cassandra.db.ReadKind.TRACKED_DATA; import static org.apache.cassandra.exceptions.RequestFailureReason.UNKNOWN; import static org.apache.cassandra.locator.InetAddressAndPort.Serializer.inetAddressAndPortSerializer; import static org.apache.cassandra.net.Verb.PAXOS2_PREPARE_REQ; @@ -141,6 +155,8 @@ public class PaxosPrepare extends PaxosRequestCallback im private static Runnable onLinearizabilityViolation; + private static final Future NO_READ_RESPONSE = ImmediateFuture.success(null); + public static final RequestHandler requestHandler = new RequestHandler(); public static final RequestSerializer requestSerializer = new RequestSerializer(); public static final ResponseSerializer responseSerializer = new ResponseSerializer(); @@ -179,12 +195,12 @@ public class PaxosPrepare extends PaxosRequestCallback im static class Success extends WithRequestedBallot { - final List> responses; + final List> responses; final boolean isReadSafe; // read responses constitute a linearizable read (though short read protection would invalidate that) final @Nullable Ballot supersededBy; // if known and READ_SUCCESS - Success(Outcome outcome, Ballot ballot, Participants participants, List> responses, boolean isReadSafe, @Nullable Ballot supersededBy) + Success(Outcome outcome, Ballot ballot, Participants participants, List> responses, boolean isReadSafe, @Nullable Ballot supersededBy) { super(outcome, participants, ballot); this.responses = responses; @@ -192,12 +208,12 @@ public class PaxosPrepare extends PaxosRequestCallback im this.supersededBy = supersededBy; } - static Success read(Ballot ballot, Participants participants, List> responses, @Nullable Ballot supersededBy) + static Success read(Ballot ballot, Participants participants, List> responses, @Nullable Ballot supersededBy) { return new Success(Outcome.READ_PERMITTED, ballot, participants, responses, true, supersededBy); } - static Success readOrWrite(Ballot ballot, Participants participants, List> responses, boolean isReadConsistent) + static Success readOrWrite(Ballot ballot, Participants participants, List> responses, boolean isReadConsistent) { return new Success(Outcome.PROMISED, ballot, participants, responses, isReadConsistent, null); } @@ -327,8 +343,9 @@ public class PaxosPrepare extends PaxosRequestCallback im private final Participants participants; - private final List> readResponses; + private final List> readResponses; private boolean haveReadResponseWithLatest; + private boolean haveTrackedDataResponseIfNeeded; private boolean haveQuorumOfPermissions; // permissions => SUCCESS or READ_SUCCESS private @Nonnull List withLatest; // promised and have latest commit private @Nullable List needLatest; // promised without having witnessed latest commit, nor yet been refreshed by us @@ -361,7 +378,7 @@ public class PaxosPrepare extends PaxosRequestCallback im // no need to commit a no-op; either it // 1) reached a majority, in which case it was agreed, had no effect and we can do nothing; or // 2) did not reach a majority, was not agreed, and was not user visible as a result so we can ignore it - if (latestAccepted.update.isEmpty()) + if (latestAccepted.isEmpty()) return false; // If we aren't newer than latestCommitted, then we're done @@ -375,17 +392,17 @@ public class PaxosPrepare extends PaxosRequestCallback im return !latestAccepted.isReproposalOf(latestCommitted); } - static PaxosPrepare prepare(Participants participants, SinglePartitionReadCommand readCommand, boolean isWrite, boolean acceptEarlyReadPermission) throws UnavailableException + static PaxosPrepare prepare(Participants participants, EmbeddableSinglePartitionReadCommand readCommand, boolean isWrite, boolean acceptEarlyReadPermission) throws UnavailableException { return prepare(null, participants, readCommand, isWrite, acceptEarlyReadPermission); } - static PaxosPrepare prepare(Ballot minimumBallot, Participants participants, SinglePartitionReadCommand readCommand, boolean isWrite, boolean acceptEarlyReadPermission) throws UnavailableException + static PaxosPrepare prepare(Ballot minimumBallot, Participants participants, EmbeddableSinglePartitionReadCommand readCommand, boolean isWrite, boolean acceptEarlyReadPermission) throws UnavailableException { return prepareWithBallot(newBallot(minimumBallot, participants.consistencyForConsensus), participants, readCommand, isWrite, acceptEarlyReadPermission); } - static PaxosPrepare prepareWithBallot(Ballot ballot, Participants participants, SinglePartitionReadCommand readCommand, boolean isWrite, boolean acceptEarlyReadPermission) + static PaxosPrepare prepareWithBallot(Ballot ballot, Participants participants, EmbeddableSinglePartitionReadCommand readCommand, boolean isWrite, boolean acceptEarlyReadPermission) { Tracing.trace("Preparing {} with read", ballot); Request request = new Request(ballot, participants.electorate, readCommand, isWrite, false); @@ -411,22 +428,107 @@ public class PaxosPrepare extends PaxosRequestCallback im /** * Submit the message to our peers, and submit it for local execution if relevant */ - static > void start(PaxosPrepare prepare, Participants participants, Message send, BiFunction selfHandler) + static > void start(PaxosPrepare prepare, Participants participants, Message send, BiFunction> selfHandler) { - boolean executeOnSelf = false; - for (int i = 0, size = participants.sizeOfPoll() ; i < size ; ++i) + if (send.payload.read != null && send.payload.read.metadata().replicationType().isTracked()) + startTracked(prepare, participants, send, selfHandler); + else + startUntracked(prepare, participants, send, selfHandler); + } + + private static > void startTracked(PaxosPrepare prepare, Participants participants, Message send, BiFunction> selfHandler) + { + if (prepare.request.read == null) + prepare.haveTrackedDataResponseIfNeeded = true; + Message selfMessage = null; + Message summaryMessage = null; + Id readId = Id.nextId(); + Replica localReplica = participants.lookup(FBUtilities.getBroadcastAddressAndPort()); + Replica dataNode = localReplica != null && localReplica.isFull() + ? localReplica + : null; + int[] summaryHostIds = new int[participants.sizeOfPoll() - 1]; // all nodes except data node + int summaryIndex = 0; + ClusterMetadata metadata = ClusterMetadata.current(); + if (dataNode == null) { - InetAddressAndPort destination = participants.voter(i); - boolean isPending = participants.electorate.isPending(destination); - logger.trace("{} to {}", send.payload, destination); - if (shouldExecuteOnSelf(destination)) - executeOnSelf = true; - else - MessagingService.instance().sendWithCallback(isPending ? withoutRead(send) : send, destination, prepare); + for (int i = 0, size = participants.sizeOfPoll() ; i < size ; i++) + { + Replica replica = participants.voterReplica(i); + if (!replica.isFull() || participants.electorate.isPending(replica.endpoint())) + continue; + dataNode = replica; + break; + } } - if (executeOnSelf) - send.verb().stage.execute(() -> prepare.executeOnSelf(send.payload, selfHandler)); + checkState(dataNode != null, "Couldn't find a data node to use"); + int dataNodeId = metadata.directory.peerId(dataNode.endpoint()).id(); + for (int i = 0, size = participants.sizeOfPoll() ; i < size ; ++i) + { + Replica replica = participants.voterReplica(i); + if (replica != dataNode && !participants.electorate.isPending(replica.endpoint())) + summaryHostIds[summaryIndex++] = metadata.directory.peerId(replica.endpoint()).id(); + } + + for (int i = 0, size = participants.sizeOfPoll() ; i < size ; ++i) + { + Replica replica = participants.voterReplica(i); + InetAddressAndPort destination = replica.endpoint(); + Message toSendThisTime; + + if (participants.electorate.isPending(destination)) + toSendThisTime = withoutRead(send); + else if (replica == dataNode) + toSendThisTime = withTrackedDataRequest(send, readId, participants.consistencyLevel(), dataNodeId, summaryHostIds); + else + { + if (summaryMessage == null) + summaryMessage = withTrackedSummaryRequest(send, readId, dataNodeId, summaryHostIds); + toSendThisTime = summaryMessage; + } + + logger.trace("{} to {}", toSendThisTime.payload, destination); + if (shouldExecuteOnSelf(destination)) + selfMessage = toSendThisTime; + else + MessagingService.instance().sendWithCallback(toSendThisTime, destination, prepare); + } + + if (selfMessage != null) + { + Message selfMessageFinal = selfMessage; + send.verb().stage.execute(() -> prepare.executeOnSelfAsync(selfMessageFinal.payload, new RequestTime(selfMessageFinal.createdAtNanos()), selfHandler)); + } + } + + private static > void startUntracked(PaxosPrepare prepare, Participants participants, Message send, BiFunction> selfHandler) + { + prepare.haveTrackedDataResponseIfNeeded = true; + Message selfMessage = null; + + for (int i = 0, size = participants.sizeOfPoll() ; i < size ; ++i) + { + Replica replica = participants.voterReplica(i); + checkState(!replica.isTransient(), "Transient replication only supported with mutation tracking"); + InetAddressAndPort destination = replica.endpoint(); + Message toSendThisTime = send; + + if (participants.electorate.isPending(destination)) + toSendThisTime = withoutRead(send); + + logger.trace("{} to {}", toSendThisTime.payload, destination); + if (shouldExecuteOnSelf(destination)) + selfMessage = toSendThisTime; + else + MessagingService.instance().sendWithCallback(toSendThisTime, destination, prepare); + } + + if (selfMessage != null) + { + Message selfMessageFinal = selfMessage; + send.verb().stage.execute(() -> prepare.executeOnSelfAsync(selfMessageFinal.payload, new RequestTime(selfMessageFinal.createdAtNanos()), selfHandler)); + } } // TODO: extend Sync? @@ -450,6 +552,11 @@ public class PaxosPrepare extends PaxosRequestCallback im } } + private boolean isTracked() + { + return request.read != null && request.read.metadata().replicationType().isTracked(); + } + private boolean isDone() { return outcome != null; @@ -581,7 +688,7 @@ public class PaxosPrepare extends PaxosRequestCallback im } } - if (!haveQuorumOfPermissions) + if (!haveQuorumOfPermissions || !haveTrackedDataResponseIfNeeded) { Committed newLatestCommitted = permitted.latestCommitted; if (newLatestCommitted.ballot.uuidTimestamp() < maxLowBound) newLatestCommitted = Committed.none(request.partitionKey, request.table); @@ -656,9 +763,9 @@ public class PaxosPrepare extends PaxosRequestCallback im } haveQuorumOfPermissions |= withLatest() + needLatest() >= participants.sizeOfConsensusQuorum; - if (haveQuorumOfPermissions) + if (haveQuorumOfPermissions && haveTrackedDataResponseIfNeeded) { - if (request.read != null && readResponses.size() < participants.sizeOfReadQuorum) + if (request.read != null && !isTracked() && readResponses.size() < participants.sizeOfReadQuorum) throw new IllegalStateException("Insufficient read responses: " + readResponses + "; need " + participants.sizeOfReadQuorum); if (!hasOnlyPromises && !hasProposalStability) @@ -684,8 +791,10 @@ public class PaxosPrepare extends PaxosRequestCallback im { refreshStaleParticipants(); // if an optimistic read is possible, and we are performing a read, - // we can safely answer immediately without waiting for the refresh - if (hasProposalStability && acceptEarlyReadPermission) + // we can safely answer immediately without waiting for the refresh. + // Check isDone() in case refreshStaleParticipants() completed synchronously + // and already signaled done via onRefreshSuccess() callback. + if (!isDone() && hasProposalStability && acceptEarlyReadPermission) signalDone(Outcome.READ_PERMITTED); } @@ -758,7 +867,7 @@ public class PaxosPrepare extends PaxosRequestCallback im // or in the case that we have an empty proposal accepted, since that will not be committed // in theory in this case we could now restart refreshStaleParticipants, but this would // unnecessarily complicate the logic so instead we accept that we will unnecessarily re-propose - if (latestAccepted != null && latestAccepted.update.isEmpty() && latestAccepted.isAfter(permitted.latestCommitted)) + if (latestAccepted != null && latestAccepted.isEmpty() && latestAccepted.isAfter(permitted.latestCommitted)) return false; // or in the case that both are older than the most recent repair low bound), in which case a topology change @@ -786,7 +895,7 @@ public class PaxosPrepare extends PaxosRequestCallback im } } - long gcGraceMicros = TimeUnit.SECONDS.toMicros(permitted.latestCommitted.update.metadata().params.gcGraceSeconds); + long gcGraceMicros = TimeUnit.SECONDS.toMicros(permitted.latestCommitted.metadata().params.gcGraceSeconds); // paxos repair uses stale ballots, so comparing against request.ballot time will not completely prevent false // positives, since compaction may have removed paxos metadata on some nodes and not others. It's also possible // clock skew has placed the ballot to repair in the future, so we use now or the ballot, whichever is higher. @@ -843,8 +952,10 @@ public class PaxosPrepare extends PaxosRequestCallback im * * Must be invoked while owning lock */ - private void addReadResponse(ReadResponse response, InetAddressAndPort from) + private void addReadResponse(IReadResponse response, InetAddressAndPort from) { + if (response.kind() == TRACKED_DATA) + haveTrackedDataResponseIfNeeded = true; readResponses.add(Message.synthetic(from, PAXOS2_PREPARE_RSP, response)); } @@ -961,13 +1072,13 @@ public class PaxosPrepare extends PaxosRequestCallback im { final Ballot ballot; final Electorate electorate; - final SinglePartitionReadCommand read; + final EmbeddableSinglePartitionReadCommand read; final boolean isForWrite; final DecoratedKey partitionKey; final TableMetadata table; final boolean isForRecovery; - AbstractRequest(Ballot ballot, Electorate electorate, SinglePartitionReadCommand read, boolean isForWrite, boolean isForRecovery) + AbstractRequest(Ballot ballot, Electorate electorate, EmbeddableSinglePartitionReadCommand read, boolean isForWrite, boolean isForRecovery) { this.ballot = ballot; this.electorate = electorate; @@ -991,6 +1102,10 @@ public class PaxosPrepare extends PaxosRequestCallback im abstract R withoutRead(); + abstract R asTrackedDataRequest(Id id, ConsistencyLevel consistencyLevel, int dataNode, int[] summaryNodes); + + abstract R asTrackedSummaryRequest(Id id, int dataNode, int[] summaryNodes); + public String toString() { return "Prepare(" + ballot + ')'; @@ -999,7 +1114,7 @@ public class PaxosPrepare extends PaxosRequestCallback im static class Request extends AbstractRequest { - Request(Ballot ballot, Electorate electorate, SinglePartitionReadCommand read, boolean isWrite, boolean isForRecovery) + Request(Ballot ballot, Electorate electorate, EmbeddableSinglePartitionReadCommand read, boolean isWrite, boolean isForRecovery) { super(ballot, electorate, read, isWrite, isForRecovery); } @@ -1018,6 +1133,18 @@ public class PaxosPrepare extends PaxosRequestCallback im { return "Prepare(" + ballot + ')'; } + + @Override + public Request asTrackedDataRequest(Id id, ConsistencyLevel consistencyLevel, int dataNode, int[] summaryNodes) + { + return new Request(ballot, electorate, new TrackedRead.DataRequest(id, (SinglePartitionReadCommand)read, dataNode, summaryNodes, consistencyLevel), isForWrite, isForRecovery); + } + + @Override + public Request asTrackedSummaryRequest(Id id, int dataNode, int[] summaryNodes) + { + return new Request(ballot, electorate, new TrackedRead.SummaryRequest(id, (SinglePartitionReadCommand)read, dataNode, summaryNodes), isForWrite, isForRecovery); + } } static class Response @@ -1049,7 +1176,7 @@ public class PaxosPrepare extends PaxosRequestCallback im // a proposal that has been accepted but not committed, i.e. must be null or > latestCommit @Nullable final Accepted latestAcceptedButNotCommitted; final Committed latestCommitted; - @Nullable final ReadResponse readResponse; + @Nullable final IReadResponse readResponse; // latestAcceptedButNotCommitted and latestCommitted were the same before and after the read occurred, and no incomplete promise was witnessed final boolean hadProposalStability; // it would be great if we could get rid of this, but probably we need to preserve for migration purposes @@ -1057,7 +1184,7 @@ public class PaxosPrepare extends PaxosRequestCallback im @Nullable final Ballot supersededBy; final Epoch electorateEpoch; - Permitted(MaybePromise.Outcome outcome, long lowBound, @Nullable Accepted latestAcceptedButNotCommitted, Committed latestCommitted, @Nullable ReadResponse readResponse, boolean hadProposalStability, Map gossipInfo, Epoch electorateEpoch, @Nullable Ballot supersededBy) + Permitted(MaybePromise.Outcome outcome, long lowBound, @Nullable Accepted latestAcceptedButNotCommitted, Committed latestCommitted, @Nullable IReadResponse readResponse, boolean hadProposalStability, Map gossipInfo, Epoch electorateEpoch, @Nullable Ballot supersededBy) { super(outcome); this.lowBound = lowBound; @@ -1103,11 +1230,26 @@ public class PaxosPrepare extends PaxosRequestCallback im try { - Response response = execute(message.payload, message.from()); + Future response = execute(message.payload, new RequestTime(message.createdAtNanos())); if (response == null) + { MessagingService.instance().respondWithFailure(UNKNOWN, message); + } + else if (response.isDone()) + { + // TODO This will probably require exception unwrapping to get the correct error handling up to the message handler + // This also runs on the mutation stage and is waiting on distributed things which is sus + MessagingService.instance().respond(getUnchecked(response), message); + } else - MessagingService.instance().respond(response, message); + { + response.addCallback((success, failure) -> { + if (failure != null) + MessagingService.instance().respondWithFailure(RequestFailure.forException(failure), message); + else + MessagingService.instance().respond(success, message); + }); + } } catch (RetryOnDifferentSystemException e) { @@ -1115,9 +1257,9 @@ public class PaxosPrepare extends PaxosRequestCallback im } } - static Response execute(AbstractRequest request, InetAddressAndPort from) + static Future execute(Request request, RequestTime requestTime) { - if (!isInRangeAndShouldProcess(from, request.partitionKey, request.table, request.read != null)) + if (!isInRangeAndShouldProcess(request.partitionKey, request.table, request.read != null)) return null; long start = nanoTime(); @@ -1135,12 +1277,13 @@ public class PaxosPrepare extends PaxosRequestCallback im // Also need to know max HLC in order to accept this ballot long maxHLC = keyMigrationState.maybePerformAccordToPaxosKeyMigration(request.isForWrite); if (maxHLC >= request.ballot.unixMicros()) - return new Rejected(Ballot.atUnixMicrosWithLsb(maxHLC + 1, 0, request.ballot.flag())); + return ImmediateFuture.success(new Rejected(Ballot.atUnixMicrosWithLsb(maxHLC + 1, 0, request.ballot.flag()))); } try (PaxosState state = get(request.partitionKey, request.table)) { - return execute(request, state, cm); + // TODO this 1000% looks like the wrong way to propagate the deadline to TrackedRead + return execute(requestTime, request, state, cm); } } finally @@ -1149,7 +1292,7 @@ public class PaxosPrepare extends PaxosRequestCallback im } } - static Response execute(AbstractRequest request, PaxosState state, ClusterMetadata cm) + static Future execute(RequestTime requestTime, AbstractRequest request, PaxosState state, ClusterMetadata cm) { MaybePromise result = state.promiseIfNewer(request.ballot, request.isForWrite); switch (result.outcome) @@ -1165,7 +1308,7 @@ public class PaxosPrepare extends PaxosRequestCallback im Map gossipInfo = verifyElectorate(request.electorate, localElectorate); // TODO when 6.0 is the minimum supported version we can modify verifyElectorate to just return this epoch Epoch electorateEpoch = gossipInfo.isEmpty() ? Epoch.EMPTY : localElectorate.createdAt; - ReadResponse readResponse = null; + Future readResponseFuture = NO_READ_RESPONSE; // Check we cannot race with a proposal, i.e. that we have not made a promise that // could be in the process of making a proposal. If a majority of nodes have made no such promise @@ -1179,7 +1322,7 @@ public class PaxosPrepare extends PaxosRequestCallback im Ballot mostRecentCommit = result.before.accepted != null && result.before.accepted.ballot.compareTo(result.before.committed.ballot) > 0 - && result.before.accepted.update.isEmpty() + && result.before.accepted.isEmpty() ? result.before.accepted.ballot : result.before.committed.ballot; boolean hasProposalStability = mostRecentCommit.equals(result.before.promisedWrite) @@ -1187,15 +1330,7 @@ public class PaxosPrepare extends PaxosRequestCallback im if (request.read != null) { - SinglePartitionReadCommand readCommand = request.read; - // Allows txn recovery to read even if it would be blocked by migration away from Paxos - if (request.isForRecovery) - readCommand = readCommand.withTransactionalSettings(false, readCommand.nowInSec()); - try (ReadExecutionController executionController = readCommand.executionController(); - UnfilteredPartitionIterator iterator = readCommand.executeLocally(executionController, cm)) - { - readResponse = readCommand.createResponse(iterator, executionController.getRepairedDataInfo()); - } + readResponseFuture = request.read.isTracked() ? readTracked((TrackedRead.Request)request.read, requestTime, cm) : readUntracked((SinglePartitionReadCommand)request.read, request.isForRecovery); if (hasProposalStability) { @@ -1212,20 +1347,56 @@ public class PaxosPrepare extends PaxosRequestCallback im ColumnFamilyStore cfs = Schema.instance.getColumnFamilyStoreInstance(request.table.id); long lowBound = cfs.getPaxosRepairLowBound(request.partitionKey).uuidTimestamp(); - return new Permitted(result.outcome, lowBound, acceptedButNotCommitted, committed, readResponse, hasProposalStability, gossipInfo, electorateEpoch, supersededBy); - + boolean hasProposalStabilityFinal = hasProposalStability; + return readResponseFuture.map(readResponse -> new Permitted(result.outcome, lowBound, acceptedButNotCommitted, committed, readResponse, hasProposalStabilityFinal, gossipInfo, electorateEpoch, supersededBy)); case REJECT: - return new Rejected(result.supersededBy()); + return ImmediateFuture.success(new Rejected(result.supersededBy())); default: throw new IllegalStateException(); } } + + private static Future readTracked(TrackedRead.Request read, RequestTime requestTime, ClusterMetadata cm) + { + // TODO(accord): This doesn't honor reads for recovery when going down the tracked path which Accord needs + if (read.kind() == TRACKED_DATA) + return readTrackedData((DataRequest)read, requestTime, cm); + else + { + return readTrackedSummary((SummaryRequest)read, requestTime, cm); + } + } + + private static Future readTrackedSummary(SummaryRequest read, RequestTime requestTime, ClusterMetadata cm) + { + return read.executeLocally(read, cm, requestTime); + } + + private static Future readTrackedData(DataRequest read, RequestTime requestTime, ClusterMetadata cm) + { + return read.executeLocally(read, cm, requestTime); + } + + private static Future readUntracked(SinglePartitionReadCommand read, boolean isForRecovery) + { + // Allows txn recovery to read even if it would be blocked by migration away from Paxos + if (isForRecovery) + read = read.withTransactionalSettings(false, read.nowInSec()); + + ReadResponse readResponse; + try (ReadExecutionController executionController = read.executionController(); + UnfilteredPartitionIterator iterator = read.executeLocally(executionController)) + { + readResponse = read.createResponse(iterator, executionController.getRepairedDataInfo()); + } + return ImmediateFuture.success(readResponse); + } } static abstract class AbstractRequestSerializer, T> implements IVersionedSerializer { - abstract R construct(T param, Ballot ballot, Electorate electorate, SinglePartitionReadCommand read, boolean isWrite, boolean isForRecovery); + abstract R construct(T param, Ballot ballot, Electorate electorate, EmbeddableSinglePartitionReadCommand read, boolean isWrite, boolean isForRecovery); abstract R construct(T param, Ballot ballot, Electorate electorate, DecoratedKey partitionKey, TableMetadata table, boolean isWrite, boolean isForRecovery); @Override @@ -1237,7 +1408,7 @@ public class PaxosPrepare extends PaxosRequestCallback im if (request.read != null) { - ReadCommand.serializer.serialize(request.read, out, version); + EmbeddableSinglePartitionReadCommand.serializer.serialize(request.read, out, version); } else { @@ -1255,7 +1426,7 @@ public class PaxosPrepare extends PaxosRequestCallback im byte flag = in.readByte(); if ((flag & 1) != 0) { - SinglePartitionReadCommand readCommand = (SinglePartitionReadCommand) ReadCommand.serializer.deserialize(in, version); + EmbeddableSinglePartitionReadCommand readCommand = EmbeddableSinglePartitionReadCommand.serializer.deserialize(in, version); boolean isForRecovery = false; if (version >= MessagingService.VERSION_60) isForRecovery = in.readBoolean(); @@ -1278,7 +1449,7 @@ public class PaxosPrepare extends PaxosRequestCallback im long size = Ballot.sizeInBytes() + Electorate.serializer.serializedSize(request.electorate, version) + 1 + (request.read != null - ? ReadCommand.serializer.serializedSize(request.read, version) + ? EmbeddableSinglePartitionReadCommand.serializer.serializedSize(request.read, version) : request.table.id.serializedSize() + DecoratedKey.serializer.serializedSize(request.partitionKey, version)); if (version >= MessagingService.VERSION_60) @@ -1289,7 +1460,7 @@ public class PaxosPrepare extends PaxosRequestCallback im public static class RequestSerializer extends AbstractRequestSerializer { - Request construct(Object ignore, Ballot ballot, Electorate electorate, SinglePartitionReadCommand read, boolean isWrite, boolean isForRecovery) + Request construct(Object ignore, Ballot ballot, Electorate electorate, EmbeddableSinglePartitionReadCommand read, boolean isWrite, boolean isForRecovery) { return new Request(ballot, electorate, read, isWrite, isForRecovery); } @@ -1329,7 +1500,7 @@ public class PaxosPrepare extends PaxosRequestCallback im Accepted.serializer.serialize(promised.latestAcceptedButNotCommitted, out, version); Committed.serializer.serialize(promised.latestCommitted, out, version); if (promised.readResponse != null) - ReadResponse.serializer.serialize(promised.readResponse, out, version); + IReadResponse.serializer.serialize(promised.readResponse, out, version); serializeMap(promised.gossipInfo, out, version, inetAddressAndPortSerializer, EndpointState.nullableSerializer); if (version >= MessagingService.VERSION_60) Epoch.messageSerializer.serialize(promised.electorateEpoch, out, version); @@ -1351,7 +1522,7 @@ public class PaxosPrepare extends PaxosRequestCallback im long lowBound = in.readUnsignedVInt(); Accepted acceptedNotCommitted = (flags & 2) != 0 ? Accepted.serializer.deserialize(in, version) : null; Committed committed = Committed.serializer.deserialize(in, version); - ReadResponse readResponse = (flags & 4) != 0 ? ReadResponse.serializer.deserialize(in, version) : null; + IReadResponse readResponse = (flags & 4) != 0 ? IReadResponse.serializer.deserialize(in, version) : null; Map gossipInfo = deserializeMap(in, version, inetAddressAndPortSerializer, EndpointState.nullableSerializer); Epoch electorateEpoch = version >= MessagingService.VERSION_60 ? Epoch.messageSerializer.deserialize(in, version) : Epoch.EMPTY; MaybePromise.Outcome outcome = (flags & 16) != 0 ? PERMIT_READ : PROMISE; @@ -1376,7 +1547,7 @@ public class PaxosPrepare extends PaxosRequestCallback im size += VIntCoding.computeUnsignedVIntSize(permitted.lowBound) + (permitted.latestAcceptedButNotCommitted == null ? 0 : Accepted.serializer.serializedSize(permitted.latestAcceptedButNotCommitted, version)) + Committed.serializer.serializedSize(permitted.latestCommitted, version) - + (permitted.readResponse == null ? 0 : ReadResponse.serializer.serializedSize(permitted.readResponse, version)) + + (permitted.readResponse == null ? 0 : IReadResponse.serializer.serializedSize(permitted.readResponse, version)) + serializedMapSize(permitted.gossipInfo, version, inetAddressAndPortSerializer, EndpointState.nullableSerializer) + (version >= MessagingService.VERSION_60 ? Epoch.messageSerializer.serializedSize(permitted.electorateEpoch, version) : 0) + (permitted.outcome == PERMIT_READ ? Ballot.sizeInBytes() : 0); @@ -1394,6 +1565,22 @@ public class PaxosPrepare extends PaxosRequestCallback im return send.withPayload(send.payload.withoutRead()); } + static > Message withTrackedDataRequest(Message send, Id id, ConsistencyLevel cl, int dataNode, int[] summaryNodes) + { + if (send.payload.read == null) + return send; + + return send.withPayload(send.payload.asTrackedDataRequest(id, cl, dataNode, summaryNodes)); + } + + static > Message withTrackedSummaryRequest(Message send, Id id, int dataNode, int[] summaryNodes) + { + if (send.payload.read == null) + return send; + + return send.withPayload(send.payload.asTrackedSummaryRequest(id, dataNode, summaryNodes)); + } + public static void setOnLinearizabilityViolation(Runnable runnable) { assert onLinearizabilityViolation == null || runnable == null; diff --git a/src/java/org/apache/cassandra/service/paxos/PaxosPrepareRefresh.java b/src/java/org/apache/cassandra/service/paxos/PaxosPrepareRefresh.java index ffc9aa610f..d14350728b 100644 --- a/src/java/org/apache/cassandra/service/paxos/PaxosPrepareRefresh.java +++ b/src/java/org/apache/cassandra/service/paxos/PaxosPrepareRefresh.java @@ -24,18 +24,24 @@ import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +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.WriteTimeoutException; +import org.apache.cassandra.gms.FailureDetector; import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.locator.Replica; import org.apache.cassandra.net.IVerbHandler; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.net.RequestCallbackWithFailure; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.replication.MutationId; +import org.apache.cassandra.replication.MutationTrackingService; import org.apache.cassandra.service.paxos.Commit.Agreed; import org.apache.cassandra.service.paxos.Commit.Committed; import org.apache.cassandra.tcm.ClusterMetadata; @@ -72,17 +78,113 @@ public class PaxosPrepareRefresh implements RequestCallbackWithFailure send; + private Message send; private final Callbacks callbacks; + private final Paxos.Participants participants; + private final boolean isUrgent; public PaxosPrepareRefresh(Ballot prepared, Paxos.Participants participants, Committed latestCommitted, Callbacks callbacks) { this.callbacks = callbacks; - this.send = Message.out(PAXOS2_PREPARE_REFRESH_REQ, new Request(prepared, latestCommitted), participants.isUrgent()); + this.participants = participants; + this.isUrgent = participants.isUrgent(); + this.send = Message.out(PAXOS2_PREPARE_REFRESH_REQ, new Request(prepared, latestCommitted), isUrgent); } + /* + * onRefreshSuccess can be called from this method synchronously due to local application at this node + * so the caller must be aware that in PaxosPrepare completion may already have been signaled. + */ void refresh(List refresh) { + // Check if forwarding is needed for tracked keyspaces + Committed commit = send.payload.missingCommit; + + if (commit.metadata().replicationType().isTracked() && commit.mutation.id().isNone()) + { + // Check if we can generate mutation ID locally (are we a replica?) + Replica localReplica = participants.all.byEndpoint().get(getBroadcastAddressAndPort()); + + if (localReplica != null) + { + // We ARE a replica - generate mutation ID and update the commit + String keyspaceName = commit.metadata().keyspace; + MutationId mutationId = MutationTrackingService.instance.nextMutationId(keyspaceName, commit.partitionKey().getToken()); + Mutation mutationWithId = commit.makeMutation(mutationId); + Committed commitWithId = new Commit.Committed(commit.ballot, mutationWithId); + + // Update the message payload with the new commit + this.send = Message.out(PAXOS2_PREPARE_REFRESH_REQ, + new Request(send.payload.promised, commitWithId), + isUrgent); + + // For tracked keyspaces, we MUST ALWAYS write to the local journal since we generated the mutation ID. + // This is required for retry purposes: if a remote target fails, the ActiveLogReconciler will try + // to look up the mutation in the local journal. The node that generated the mutation ID is the "owner" + // and must have the mutation available for retries. + // + // We do this BEFORE the main refresh loop to ensure the mutation is in the journal before any + // failure callback can trigger reconciliation. + Response localResponse = null; + try + { + localResponse = RequestHandler.execute(this.send.payload, getBroadcastAddressAndPort()); + if (localResponse == null) + logger.warn("Local execution failed for tracked mutation {}", mutationId); + } + catch (Exception e) + { + logger.warn("Exception writing tracked mutation {} locally", mutationId, e); + } + + // If self is in the refresh list, report the local execution result to callbacks + // We need to do this because the main loop will skip self for tracked keyspaces + for (int i = 0, size = refresh.size(); i < size; ++i) + { + if (shouldExecuteOnSelf(refresh.get(i))) + { + if (localResponse != null) + callbacks.onRefreshSuccess(localResponse.isSupersededBy, getBroadcastAddressAndPort()); + else + callbacks.onRefreshFailure(getBroadcastAddressAndPort(), RequestFailure.UNKNOWN); + break; + } + } + } + else + { + // We're NOT a replica - forward to a replica + forwardRefresh(refresh); + return; + } + } + + // For tracked keyspaces where we generated the ID above, we already wrote locally. + // For tracked keyspaces where the ID was already present, we still need to ensure local execution. + boolean isTracked = !send.payload.missingCommit.mutation.id().isNone(); + + // If we just generated the ID above, we already wrote locally - check by examining the original commit + boolean alreadyWroteLocally = commit.metadata().replicationType().isTracked() + && commit.mutation.id().isNone() + && participants.all.byEndpoint().get(getBroadcastAddressAndPort()) != null; + boolean localExecutedSync = alreadyWroteLocally; + + // For tracked keyspaces where we DIDN'T generate the ID (it was already present), we still need to + // execute locally BEFORE sending to remotes if self is in the refresh list. + if (isTracked && !alreadyWroteLocally) + { + for (int i = 0, size = refresh.size(); i < size; ++i) + { + if (shouldExecuteOnSelf(refresh.get(i))) + { + executeOnSelf(); // SYNCHRONOUS - journal write completes here + localExecutedSync = true; + break; + } + } + } + + // Now send to remote nodes (and record local execution for non-tracked keyspaces) boolean executeOnSelf = false; for (int i = 0, size = refresh.size(); i < size ; ++i) { @@ -95,15 +197,104 @@ public class PaxosPrepareRefresh implements RequestCallbackWithFailure refreshTargets) + { + // Find a live replica to forward to (that's not us) + InetAddressAndPort targetReplica = null; + for (Replica replica : participants.all) + { + if (!replica.endpoint().equals(getBroadcastAddressAndPort()) && + FailureDetector.instance.isAlive(replica.endpoint())) + { + targetReplica = replica.endpoint(); + break; + } + } + + if (targetReplica == null) + { + logger.error("No live replica available to forward PaxosPrepareRefresh for {}", + send.payload.missingCommit.partitionKey()); + // Report failure for all refresh targets + for (InetAddressAndPort target : refreshTargets) + callbacks.onRefreshFailure(target, RequestFailure.UNKNOWN); + return; + } + + logger.debug("Forwarding PaxosPrepareRefresh to replica {} for mutation ID generation", targetReplica); + Tracing.trace("Forwarding PaxosPrepareRefresh to replica {}", targetReplica); + + // Create forward request with refresh targets + PrepareRefreshForwardRequest forwardRequest = new PrepareRefreshForwardRequest( + send.payload.promised, + send.payload.missingCommit, + refreshTargets, + isUrgent + ); + + Message message = Message.out( + Verb.PAXOS_PREPARE_REFRESH_FORWARD_REQ, forwardRequest, isUrgent); + + // Send and handle response + MessagingService.instance().sendWithCallback(message, targetReplica, + new ForwardCallback(refreshTargets)); + } + + /** + * Callback for forwarded refresh operations. + * Translates forward response to individual refresh callbacks. + */ + private class ForwardCallback implements RequestCallbackWithFailure + { + private final List refreshTargets; + + ForwardCallback(List refreshTargets) + { + this.refreshTargets = refreshTargets; + } + + @Override + public void onResponse(Message message) + { + PrepareRefreshForwardResponse response = message.payload; + // Report results for each target + for (int i = 0; i < refreshTargets.size(); i++) + { + InetAddressAndPort target = refreshTargets.get(i); + Ballot supersededBy = response.supersededBy.get(i); + callbacks.onRefreshSuccess(supersededBy, target); + } + } + + @Override + public void onFailure(InetAddressAndPort from, RequestFailure reason) + { + // Report failure for all targets + for (InetAddressAndPort target : refreshTargets) + callbacks.onRefreshFailure(target, reason); + } + } + @Override public void onFailure(InetAddressAndPort from, RequestFailure reason) { @@ -148,7 +339,7 @@ public class PaxosPrepareRefresh implements RequestCallbackWithFailure> * or for the present status if the time elapses without a final result being reached. * @param waitForNoSideEffect if true, on failure we will wait until we can say with certainty there are no side effects * or until we know we will never be able to determine this with certainty - * @param isForRecovery if true the value being proposed is not a new value it is a value from an existing in flight proposal - * and will be allowed to proceed even if the key is migrating to a different consensus protocol */ static Paxos.Async propose(Proposal proposal, Paxos.Participants participants, boolean waitForNoSideEffect) { - if (waitForNoSideEffect && proposal.update.isEmpty()) + if (waitForNoSideEffect && proposal.isEmpty()) waitForNoSideEffect = false; // by definition this has no "side effects" (besides linearizing the operation) // to avoid unnecessary object allocations we extend PaxosPropose to implements Paxos.Async @@ -211,7 +209,7 @@ public class PaxosPropose> static > T propose(Proposal proposal, Paxos.Participants participants, boolean waitForNoSideEffect, T onDone) { - if (waitForNoSideEffect && proposal.update.isEmpty()) + if (waitForNoSideEffect && proposal.isEmpty()) waitForNoSideEffect = false; // by definition this has no "side effects" (besides linearizing the operation) PaxosPropose propose = new PaxosPropose<>(proposal, participants.sizeOfPoll(), participants.sizeOfConsensusQuorum, waitForNoSideEffect, onDone); @@ -416,7 +414,7 @@ public class PaxosPropose> try { - AcceptResult acceptResult = execute(message.payload.proposal, message.from()); + AcceptResult acceptResult = execute(message.payload.proposal); if (acceptResult == null) MessagingService.instance().respondWithFailure(UNKNOWN, message); else @@ -429,9 +427,9 @@ public class PaxosPropose> } } - public static AcceptResult execute(Proposal proposal, InetAddressAndPort from) + public static AcceptResult execute(Proposal proposal) { - if (!Paxos.isInRangeAndShouldProcess(from, proposal.update.partitionKey(), proposal.update.metadata(), false)) + if (!Paxos.isInRangeAndShouldProcess(proposal.partitionKey(), proposal.metadata(), false)) return null; long start = nanoTime(); @@ -441,7 +439,7 @@ public class PaxosPropose> } finally { - Keyspace.openAndGetStore(proposal.update.metadata()).metric.casPropose.addNano(nanoTime() - start); + Keyspace.openAndGetStore(proposal.metadata()).metric.casPropose.addNano(nanoTime() - start); } } } diff --git a/src/java/org/apache/cassandra/service/paxos/PaxosRepair.java b/src/java/org/apache/cassandra/service/paxos/PaxosRepair.java index 5948086394..a68ac4e54b 100644 --- a/src/java/org/apache/cassandra/service/paxos/PaxosRepair.java +++ b/src/java/org/apache/cassandra/service/paxos/PaxosRepair.java @@ -395,7 +395,7 @@ public class PaxosRepair extends AbstractPaxosRepair return retry(this); case SUCCESS: - if (proposal.update.isEmpty()) + if (proposal.isEmpty()) { logger.trace("PaxosRepair of {} complete after successful empty proposal", partitionKey()); return DONE; @@ -603,7 +603,7 @@ public class PaxosRepair extends AbstractPaxosRepair public void doVerb(Message message) { PaxosRepair.Request request = message.payload; - if (!isInRangeAndShouldProcess(message.from(), request.partitionKey, request.table, false)) + if (!isInRangeAndShouldProcess(request.partitionKey, request.table, false)) { MessagingService.instance().respondWithFailure(UNKNOWN, message); return; diff --git a/src/java/org/apache/cassandra/service/paxos/PaxosRequestCallback.java b/src/java/org/apache/cassandra/service/paxos/PaxosRequestCallback.java index e9031a73d6..d40b3b68fe 100644 --- a/src/java/org/apache/cassandra/service/paxos/PaxosRequestCallback.java +++ b/src/java/org/apache/cassandra/service/paxos/PaxosRequestCallback.java @@ -19,6 +19,7 @@ package org.apache.cassandra.service.paxos; import java.util.function.BiFunction; +import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,10 +33,12 @@ import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.Message; import org.apache.cassandra.service.FailureRecordingCallback; import org.apache.cassandra.tcm.ClusterMetadataService; -import org.apache.cassandra.utils.TriFunction; +import org.apache.cassandra.utils.concurrent.Future; +import static org.apache.cassandra.exceptions.RequestFailure.RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM; import static org.apache.cassandra.exceptions.RequestFailure.TIMEOUT; import static org.apache.cassandra.exceptions.RequestFailure.UNKNOWN; +import static com.google.common.util.concurrent.Futures.getUnchecked; import static org.apache.cassandra.utils.FBUtilities.getBroadcastAddressAndPort; public abstract class PaxosRequestCallback extends FailureRecordingCallback @@ -53,12 +56,12 @@ public abstract class PaxosRequestCallback extends FailureRecordingCallback void executeOnSelf(I parameter, BiFunction execute) + protected void executeOnSelf(I parameter, Function execute) { T response; try { - response = execute.apply(parameter, getBroadcastAddressAndPort()); + response = execute.apply(parameter); if (response == null) return; } @@ -80,35 +83,59 @@ public abstract class PaxosRequestCallback extends FailureRecordingCallback void executeOnSelf(I parameter1, J parameter2, TriFunction execute) + protected void executeOnSelfAsync(I parameter1, J parameter2, BiFunction> execute) { - T response; try { - response = execute.apply(parameter1, parameter2, getBroadcastAddressAndPort()); - if (response == null) + Future responseFuture = execute.apply(parameter1, parameter2); + if (responseFuture == null) return; - } - catch (RetryOnDifferentSystemException e) - { - onFailure(getBroadcastAddressAndPort(), RequestFailure.RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM); - return; + + if (responseFuture.isDone()) + { + // Fast path: future already complete + T response = getUnchecked(responseFuture); + onResponse(response, getBroadcastAddressAndPort()); + } + else + { + // Async path: add callback for when future completes + responseFuture.addCallback((response, failure) -> { + if (failure != null) + { + RequestFailure reason = UNKNOWN; + if (failure instanceof WriteTimeoutException) + reason = TIMEOUT; + else if (failure instanceof RetryOnDifferentSystemException) + reason = RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM; + else + logger.error("Failed to apply {} locally", parameter1, failure); + + onFailure(getBroadcastAddressAndPort(), reason); + } + else + { + onResponse(response, getBroadcastAddressAndPort()); + } + }); + } } catch (Exception ex) { RequestFailure reason = UNKNOWN; - if (ex instanceof WriteTimeoutException) reason = TIMEOUT; - else logger.error("Failed to apply {}, {} locally", parameter1, parameter2, ex); + if (ex instanceof WriteTimeoutException) + reason = TIMEOUT; + else if (ex instanceof RetryOnDifferentSystemException) + reason = RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM; + else + logger.error("Failed to apply {} locally", parameter1, ex); onFailure(getBroadcastAddressAndPort(), reason); - return; } - - onResponse(response, getBroadcastAddressAndPort()); } static boolean shouldExecuteOnSelf(InetAddressAndPort replica) { return USE_SELF_EXECUTION && replica.equals(getBroadcastAddressAndPort()); } -} +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/service/paxos/PaxosState.java b/src/java/org/apache/cassandra/service/paxos/PaxosState.java index 30929dd72d..ed2f40fcac 100644 --- a/src/java/org/apache/cassandra/service/paxos/PaxosState.java +++ b/src/java/org/apache/cassandra/service/paxos/PaxosState.java @@ -51,6 +51,9 @@ import org.apache.cassandra.exceptions.ReadTimeoutException; import org.apache.cassandra.exceptions.RequestTimeoutException; import org.apache.cassandra.exceptions.WriteTimeoutException; import org.apache.cassandra.metrics.PaxosMetrics; +import org.apache.cassandra.replication.MutationTrackingService; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.consensus.migration.ConsensusKeyMigrationState; import org.apache.cassandra.service.paxos.uncommitted.PaxosBallotTracker; @@ -191,8 +194,8 @@ public class PaxosState implements PaxosOperationLock public Snapshot(@Nonnull Ballot promised, @Nonnull Ballot promisedWrite, @Nullable Accepted accepted, @Nonnull Committed committed) { assert isAfter(promised, promisedWrite) || promised == promisedWrite; - assert accepted == null || accepted.update.partitionKey().equals(committed.update.partitionKey()); - assert accepted == null || accepted.update.metadata().id.equals(committed.update.metadata().id); + assert accepted == null || accepted.partitionKey().equals(committed.partitionKey()); + assert accepted == null || accepted.metadata().id.equals(committed.metadata().id); assert accepted == null || committed.isBefore(accepted.ballot); this.promised = promised; @@ -224,7 +227,7 @@ public class PaxosState implements PaxosOperationLock { // warn: if proposal has same timestamp as promised, we should prefer accepted // since (if different) it reached a quorum of promises; this means providing it as first argument - Ballot latest = accepted != null && !accepted.update.isEmpty() ? accepted.ballot : null; + Ballot latest = accepted != null && !accepted.isEmpty() ? accepted.ballot : null; latest = latest(latest, committed.ballot); latest = latest(latest, promisedWrite); latest = latest(latest, ballotTracker().getLowBound()); @@ -272,7 +275,7 @@ public class PaxosState implements PaxosOperationLock if (paxosStatePurging() == gc_grace) { - long expireOlderThan = SECONDS.toMicros(nowInSec - committed.update.metadata().params.gcGraceSeconds); + long expireOlderThan = SECONDS.toMicros(nowInSec - committed.metadata().params.gcGraceSeconds); isAcceptedExpired |= accepted != null && accepted.ballot.unixMicros() < expireOlderThan; isCommittedExpired |= committed.ballot.unixMicros() < expireOlderThan; } @@ -283,7 +286,7 @@ public class PaxosState implements PaxosOperationLock return new Snapshot(promised, promisedWrite, isAcceptedExpired ? null : accepted, isCommittedExpired - ? Committed.none(committed.update.partitionKey(), committed.update.metadata()) + ? Committed.none(committed.partitionKey(), committed.metadata()) : committed); } } @@ -298,7 +301,7 @@ public class PaxosState implements PaxosOperationLock public UnsafeSnapshot(@Nonnull Commit committed) { - this(new Committed(committed.ballot, committed.update)); + this(new Committed(committed.ballot, committed.mutation)); } } @@ -365,7 +368,7 @@ public class PaxosState implements PaxosOperationLock @VisibleForTesting public static PaxosState get(Commit commit) { - return get(commit.update.partitionKey(), commit.update.metadata()); + return get(commit.partitionKey(), commit.metadata()); } public static PaxosState get(DecoratedKey partitionKey, TableMetadata table) @@ -406,7 +409,7 @@ public class PaxosState implements PaxosOperationLock private static PaxosState getUnsafe(Commit commit) { - return getUnsafe(commit.update.partitionKey(), commit.update.metadata()); + return getUnsafe(commit.partitionKey(), commit.metadata()); } // don't increment the total count, as we are only using this for locking purposes when coordinating @@ -696,7 +699,7 @@ public class PaxosState implements PaxosOperationLock public static void commitDirect(Commit commit) { applyCommit(commit, null, (apply, ignore) -> { - try (PaxosState state = tryGetUnsafe(apply.update.partitionKey(), apply.update.metadata())) + try (PaxosState state = tryGetUnsafe(apply.partitionKey(), apply.metadata())) { if (state != null) currentUpdater.accumulateAndGet(state, new UnsafeSnapshot(apply), Snapshot::merge); @@ -715,7 +718,7 @@ public class PaxosState implements PaxosOperationLock // TODO: run Paxos Repair before truncate so we can excise this // The table may have been truncated since the proposal was initiated. In that case, we // don't want to perform the mutation and potentially resurrect truncated data - if (commit.ballot.unixMicros() >= SystemKeyspace.getTruncatedAt(commit.update.metadata().id)) + if (commit.ballot.unixMicros() >= SystemKeyspace.getTruncatedAt(commit.metadata().id)) { Tracing.trace("Committing proposal {}", commit); Mutation mutation = commit.makeMutation(); @@ -724,6 +727,21 @@ public class PaxosState implements PaxosOperationLock else { Tracing.trace("Not committing proposal {} as ballot timestamp predates last truncation time", commit); + + // Still acknowledge mutation ID for tracked keyspaces even though we're discarding + // This ensures the tracking service knows this replica "handled" the mutation. + // We call both startWriting and finishWriting - startWriting registers the mutation + // data (for reconciliation) and finishWriting marks it as witnessed. + // This is needed because there are cases where mutation IDs might be created for the same Paxos + // commit. + Mutation mutation = commit.makeMutation(); + if (!mutation.id().isNone()) + { + KeyspaceMetadata ksm = Schema.instance.getKeyspaceMetadata(mutation.getKeyspaceName()); + if (ksm != null && ksm.params.replicationType.isTracked() + && MutationTrackingService.instance.startWriting(mutation)) + MutationTrackingService.instance.finishWriting(mutation); + } } // for commits we save to disk first, because we can; even here though it is safe to permit later events to @@ -737,7 +755,7 @@ public class PaxosState implements PaxosOperationLock } finally { - Keyspace.openAndGetStore(commit.update.metadata()).metric.casCommit.addNano(nanoTime() - start); + Keyspace.openAndGetStore(commit.metadata()).metric.casCommit.addNano(nanoTime() - start); } } @@ -764,8 +782,8 @@ public class PaxosState implements PaxosOperationLock if (currentUpdater.compareAndSet(unsafeState, realBefore, after)) { Tracing.trace("Promising ballot {}", toPrepare.ballot); - DecoratedKey partitionKey = toPrepare.update.partitionKey(); - TableMetadata metadata = toPrepare.update.metadata(); + DecoratedKey partitionKey = toPrepare.partitionKey(); + TableMetadata metadata = toPrepare.metadata(); SystemKeyspace.savePaxosWritePromise(partitionKey, metadata, toPrepare.ballot); return new PrepareResponse(true, before.accepted == null ? Accepted.none(partitionKey, metadata) : before.accepted, before.committed); } @@ -774,14 +792,14 @@ public class PaxosState implements PaxosOperationLock { Tracing.trace("Promise rejected; {} is not sufficiently newer than {}", toPrepare, before.promised); // return the currently promised ballot (not the last accepted one) so the coordinator can make sure it uses newer ballot next time (#5667) - return new PrepareResponse(false, new Commit(before.promised, toPrepare.update), before.committed); + return new PrepareResponse(false, Commit.create(before.promised, toPrepare.mutation), before.committed); } } } } finally { - Keyspace.openAndGetStore(toPrepare.update.metadata()).metric.casPrepare.addNano(nanoTime() - start); + Keyspace.openAndGetStore(toPrepare.metadata()).metric.casPrepare.addNano(nanoTime() - start); } } @@ -827,7 +845,7 @@ public class PaxosState implements PaxosOperationLock } finally { - Keyspace.openAndGetStore(proposal.update.metadata()).metric.casPropose.addNano(nanoTime() - start); + Keyspace.openAndGetStore(proposal.metadata()).metric.casPropose.addNano(nanoTime() - start); } } diff --git a/src/java/org/apache/cassandra/service/paxos/PrepareRefreshForwardHandler.java b/src/java/org/apache/cassandra/service/paxos/PrepareRefreshForwardHandler.java new file mode 100644 index 0000000000..578ec53d9c --- /dev/null +++ b/src/java/org/apache/cassandra/service/paxos/PrepareRefreshForwardHandler.java @@ -0,0 +1,184 @@ +/* + * 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.paxos; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.Mutation; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.exceptions.RequestFailure; +import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.net.IVerbHandler; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.RequestCallbackWithFailure; +import org.apache.cassandra.replication.MutationId; +import org.apache.cassandra.replication.MutationTrackingService; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.service.paxos.Commit.Committed; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tracing.Tracing; +import org.apache.cassandra.utils.Clock; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.concurrent.CountDownLatch; + +import static org.apache.cassandra.net.Verb.PAXOS2_PREPARE_REFRESH_REQ; +import static org.apache.cassandra.service.paxos.PaxosRequestCallback.shouldExecuteOnSelf; + +/** + * Handler for forwarded PaxosPrepareRefresh requests. + * Generates a mutation ID and sends the refresh to all target nodes. + * + * This handler is invoked when a non-replica coordinator forwards the refresh + * to a full replica that can generate the mutation ID. + * + * TODO (expected): more comprehensive testing + */ +public class PrepareRefreshForwardHandler implements IVerbHandler +{ + public static final PrepareRefreshForwardHandler instance = new PrepareRefreshForwardHandler(); + private static final Logger logger = LoggerFactory.getLogger(PrepareRefreshForwardHandler.class); + + @Override + public void doVerb(Message message) + { + ClusterMetadataService.instance().fetchLogFromPeerOrCMS(message.from(), message.header.epoch); + PrepareRefreshForwardRequest request = message.payload; + + Tracing.trace("Executing forwarded PaxosPrepareRefresh for {}", request.commit.partitionKey()); + + try + { + String ksName = request.commit.metadata().keyspace; + KeyspaceMetadata ksMetadata = Schema.instance.getKeyspaceMetadata(ksName); + if (ksMetadata == null) + { + MessagingService.instance().respondWithFailure(RequestFailureReason.INCOMPATIBLE_SCHEMA, message); + logger.error("Failed to forward paxos prepare refresh for non-existent keyspace {}", ksName); + return; + } + + if (!ksMetadata.params.replicationType.isTracked()) + { + MessagingService.instance().respondWithFailure(RequestFailureReason.INCOMPATIBLE_SCHEMA, message); + logger.error("Asked to perform forwarded prepare refresh, but keyspace {} is not tracked", ksName); + return; + } + + Token token = request.commit.partitionKey().getToken(); + MutationId mutationId = MutationTrackingService.instance.nextMutationId(ksName, token); + + Mutation mutationWithId = request.commit.makeMutation(mutationId); + Committed commitWithId = new Commit.Committed(request.commit.ballot, mutationWithId); + + // Now send the refresh to all targets and collect responses + List targets = request.refreshTargets; + List supersededBy = Collections.synchronizedList(new ArrayList<>(Collections.nCopies(targets.size(), null))); + CountDownLatch latch = CountDownLatch.newCountDownLatch(targets.size()); + + Message refreshMsg = Message.out( + PAXOS2_PREPARE_REFRESH_REQ, + new PaxosPrepareRefresh.Request(request.promised, commitWithId), + request.isUrgent + ); + + // For tracked keyspaces, we MUST ALWAYS write to the local journal since we generated the mutation ID. + // This is required for retry purposes: if a remote target fails, the ActiveLogReconciler will try + // to look up the mutation in the local journal. The node that generated the mutation ID is the "owner" + // and must have the mutation available for retries. + // + // This is different from checking if self is in targets - even if we're not in targets, + // we're still the ID generator and need the mutation locally. + try + { + PaxosPrepareRefresh.RequestHandler.execute( + new PaxosPrepareRefresh.Request(request.promised, commitWithId), FBUtilities.getBroadcastAddressAndPort()); + // Note: we don't use the response since this node may not be in targets + } + catch (Exception e) + { + // Log but continue - we still need to send to targets + logger.warn("Failed to execute local commit for tracked keyspace mutation {}", mutationId, e); + } + + // Now send to remote targets + for (int i = 0; i < targets.size(); i++) + { + final int targetIndex = i; + InetAddressAndPort target = targets.get(i); + + // Check if self is in targets for response tracking (separate from the local write above) + // We need to decrement the latch for the local target since we already executed above + if (shouldExecuteOnSelf(target)) + { + latch.decrement(); + continue; + } + + RequestCallbackWithFailure callback = new RequestCallbackWithFailure<>() + { + @Override + public void onResponse(Message response) + { + supersededBy.set(targetIndex, response.payload.isSupersededBy); + latch.decrement(); + } + + @Override + public void onFailure(InetAddressAndPort from, RequestFailure reason) + { + // Leave null to indicate we didn't get a definitive answer + latch.decrement(); + } + }; + + MessagingService.instance().sendWithCallback(refreshMsg, target, callback); + } + + // Wait for all responses with timeout + long timeoutNanos = message.expiresAtNanos() - Clock.Global.nanoTime(); + boolean completed = latch.await(Math.max(0, timeoutNanos), TimeUnit.NANOSECONDS); + + if (!completed) + logger.warn("Forwarded PaxosPrepareRefresh timed out waiting for responses"); + + // Send aggregated response back to original coordinator + MessagingService.instance().respond(new PrepareRefreshForwardResponse(supersededBy), message); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + MessagingService.instance().respondWithFailure(RequestFailure.forException(e), message); + logger.error("Forwarded PaxosPrepareRefresh interrupted", e); + } + catch (Exception e) + { + MessagingService.instance().respondWithFailure(RequestFailure.forException(e), message); + logger.error("Failed to execute forwarded PaxosPrepareRefresh for {}", request.commit, e); + } + } +} diff --git a/src/java/org/apache/cassandra/service/paxos/PrepareRefreshForwardRequest.java b/src/java/org/apache/cassandra/service/paxos/PrepareRefreshForwardRequest.java new file mode 100644 index 0000000000..7c080e800a --- /dev/null +++ b/src/java/org/apache/cassandra/service/paxos/PrepareRefreshForwardRequest.java @@ -0,0 +1,90 @@ +/* + * 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.paxos; + +import java.io.IOException; +import java.util.List; + +import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.service.paxos.Commit.Committed; +import org.apache.cassandra.utils.CollectionSerializers; + +/** + * Request to forward a PaxosPrepareRefresh operation to a full replica coordinator. + * This is used when the original coordinator is not a full replica but needs to + * execute a Paxos prepare refresh for a tracked keyspace that requires MutationId generation. + * + * The full replica coordinator will generate the mutation ID and send the refresh + * to all target nodes with the same mutation ID. + */ +public class PrepareRefreshForwardRequest +{ + public static final Serializer serializer = new Serializer(); + + public final Ballot promised; + public final Committed commit; + public final List refreshTargets; + public final boolean isUrgent; + + public PrepareRefreshForwardRequest(Ballot promised, Committed commit, List refreshTargets, boolean isUrgent) + { + this.promised = promised; + this.commit = commit; + this.refreshTargets = refreshTargets; + this.isUrgent = isUrgent; + } + + public static class Serializer implements IVersionedSerializer + { + @Override + public void serialize(PrepareRefreshForwardRequest request, DataOutputPlus out, int version) throws IOException + { + request.promised.serialize(out); + Committed.serializer.serialize(request.commit, out, version); + CollectionSerializers.serializeList(request.refreshTargets, out, version, InetAddressAndPort.Serializer.inetAddressAndPortSerializer); + out.writeBoolean(request.isUrgent); + } + + @Override + public PrepareRefreshForwardRequest deserialize(DataInputPlus in, int version) throws IOException + { + Ballot promised = Ballot.deserialize(in); + Committed commit = Committed.serializer.deserialize(in, version); + List refreshTargets = CollectionSerializers.deserializeList(in, version, InetAddressAndPort.Serializer.inetAddressAndPortSerializer); + boolean isUrgent = in.readBoolean(); + + return new PrepareRefreshForwardRequest(promised, commit, refreshTargets, isUrgent); + } + + @Override + public long serializedSize(PrepareRefreshForwardRequest request, int version) + { + long size = Ballot.sizeInBytes(); + size += Committed.serializer.serializedSize(request.commit, version); + size += CollectionSerializers.serializedListSize(request.refreshTargets, version, InetAddressAndPort.Serializer.inetAddressAndPortSerializer); + size += TypeSizes.BOOL_SIZE; // isUrgent + + return size; + } + } +} diff --git a/src/java/org/apache/cassandra/service/paxos/PrepareRefreshForwardResponse.java b/src/java/org/apache/cassandra/service/paxos/PrepareRefreshForwardResponse.java new file mode 100644 index 0000000000..ec6a38b87c --- /dev/null +++ b/src/java/org/apache/cassandra/service/paxos/PrepareRefreshForwardResponse.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.service.paxos; + +import java.io.IOException; +import java.util.List; + +import org.apache.cassandra.io.IVersionedSerializer; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.utils.CollectionSerializers; +import org.apache.cassandra.utils.NullableSerializer; + +/** + * Response from a forwarded PaxosPrepareRefresh operation. + * Contains the superseding ballot for each refresh target (null if confirmed). + */ +public class PrepareRefreshForwardResponse +{ + public static final Serializer serializer = new Serializer(); + + /** + * List of superseding ballots, one per refresh target. + * Null entry means the promise was confirmed for that target. + */ + public final List supersededBy; + + public PrepareRefreshForwardResponse(List supersededBy) + { + this.supersededBy = supersededBy; + } + + public static class Serializer implements IVersionedSerializer + { + private static final IVersionedSerializer NULLABLE_BALLOT_SERIALIZER = NullableSerializer.wrap(Ballot.Serializer.instance); + + @Override + public void serialize(PrepareRefreshForwardResponse response, DataOutputPlus out, int version) throws IOException + { + CollectionSerializers.serializeList(response.supersededBy, out, version, NULLABLE_BALLOT_SERIALIZER); + } + + @Override + public PrepareRefreshForwardResponse deserialize(DataInputPlus in, int version) throws IOException + { + List supersededBy = CollectionSerializers.deserializeList(in, version, NULLABLE_BALLOT_SERIALIZER); + return new PrepareRefreshForwardResponse(supersededBy); + } + + @Override + public long serializedSize(PrepareRefreshForwardResponse response, int version) + { + return CollectionSerializers.serializedListSize(response.supersededBy, version, NULLABLE_BALLOT_SERIALIZER); + } + } +} diff --git a/src/java/org/apache/cassandra/service/paxos/PrepareResponse.java b/src/java/org/apache/cassandra/service/paxos/PrepareResponse.java index 4c7beccba1..ca7d8c7a2d 100644 --- a/src/java/org/apache/cassandra/service/paxos/PrepareResponse.java +++ b/src/java/org/apache/cassandra/service/paxos/PrepareResponse.java @@ -44,8 +44,8 @@ public class PrepareResponse public PrepareResponse(boolean promised, Commit inProgressCommit, Commit mostRecentCommit) { - assert inProgressCommit.update.partitionKey().equals(mostRecentCommit.update.partitionKey()); - assert inProgressCommit.update.metadata().id.equals(mostRecentCommit.update.metadata().id); + assert inProgressCommit.partitionKey().equals(mostRecentCommit.partitionKey()); + assert inProgressCommit.metadata().id.equals(mostRecentCommit.metadata().id); this.promised = promised; this.mostRecentCommit = mostRecentCommit; diff --git a/src/java/org/apache/cassandra/service/paxos/uncommitted/PaxosRows.java b/src/java/org/apache/cassandra/service/paxos/uncommitted/PaxosRows.java index 57fbcc05b3..546964d2ae 100644 --- a/src/java/org/apache/cassandra/service/paxos/uncommitted/PaxosRows.java +++ b/src/java/org/apache/cassandra/service/paxos/uncommitted/PaxosRows.java @@ -29,6 +29,8 @@ import com.google.common.collect.Lists; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.db.Mutation; +import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.BytesType; import org.apache.cassandra.db.marshal.Int32Type; @@ -42,6 +44,7 @@ import org.apache.cassandra.db.rows.DeserializationHelper; import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.replication.MutationId; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.schema.TableId; @@ -71,6 +74,7 @@ public class PaxosRows private static final ColumnMetadata COMMIT = paxosColumn("most_recent_commit_at", TimeUUIDType.instance); private static final ColumnMetadata COMMIT_UPDATE = paxosColumn("most_recent_commit", BytesType.instance); private static final ColumnMetadata COMMIT_VERSION = paxosColumn("most_recent_commit_version", Int32Type.instance); + private static final ColumnMetadata COMMIT_MUTATION_ID = paxosColumn("most_recent_commit_mutation_id", BytesType.instance); private PaxosRows() {} @@ -118,9 +122,22 @@ public class PaxosRows int version = getInt(row, COMMIT_VERSION, MessagingService.VERSION_40); PartitionUpdate update = getUpdate(row, COMMIT_UPDATE, version); - if (overrideTtlSeconds > 0) return new CommittedWithTTL(ballot, update, TimeUnit.MICROSECONDS.toSeconds(ballotCell.timestamp()) + overrideTtlSeconds); - else if (ballotCell.isExpiring()) return new CommittedWithTTL(ballot, update, ballotCell.localDeletionTime()); - else return new Committed(ballot, update); + + // Read mutation ID if present + MutationId mutationId = getMutationId(row); + Mutation mutation = new Mutation(mutationId, update, PotentialTxnConflicts.ALLOW); + + if (overrideTtlSeconds > 0) return new CommittedWithTTL(ballot, mutation, TimeUnit.MICROSECONDS.toSeconds(ballotCell.timestamp()) + overrideTtlSeconds); + else if (ballotCell.isExpiring()) return new CommittedWithTTL(ballot, mutation, ballotCell.localDeletionTime()); + else return new Committed(ballot, mutation); + } + + private static MutationId getMutationId(Row row) + { + Cell cell = row.getCell(COMMIT_MUTATION_ID); + if (cell == null) + return MutationId.none(); + return MutationId.fromByteBuffer(cell.buffer().duplicate()); } public static TableId getTableId(Row row) diff --git a/src/java/org/apache/cassandra/service/paxos/v1/AbstractPaxosVerbHandler.java b/src/java/org/apache/cassandra/service/paxos/v1/AbstractPaxosVerbHandler.java index 35ac9360a9..96c9439f26 100644 --- a/src/java/org/apache/cassandra/service/paxos/v1/AbstractPaxosVerbHandler.java +++ b/src/java/org/apache/cassandra/service/paxos/v1/AbstractPaxosVerbHandler.java @@ -41,14 +41,14 @@ public abstract class AbstractPaxosVerbHandler implements IVerbHandler public void doVerb(Message message) { Commit commit = message.payload; - DecoratedKey key = commit.update.partitionKey(); - if (isOutOfRangeCommit(commit.update.metadata().keyspace, key)) + DecoratedKey key = commit.partitionKey(); + if (isOutOfRangeCommit(commit.metadata().keyspace, key)) { StorageService.instance.incOutOfRangeOperationCount(); - Keyspace.open(commit.update.metadata().keyspace).metric.outOfRangeTokenPaxosRequests.inc(); + Keyspace.open(commit.metadata().keyspace).metric.outOfRangeTokenPaxosRequests.inc(); // Log at most 1 message per second - NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 1, TimeUnit.SECONDS, logMessageTemplate, message.from(), key.getToken(), commit.update.metadata().keyspace); + NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 1, TimeUnit.SECONDS, logMessageTemplate, message.from(), key.getToken(), commit.metadata().keyspace); sendFailureResponse(message); } diff --git a/src/java/org/apache/cassandra/service/reads/tracked/TrackedDataResponse.java b/src/java/org/apache/cassandra/service/reads/tracked/TrackedDataResponse.java index 01fad19fb5..b3f9d4cb4c 100644 --- a/src/java/org/apache/cassandra/service/reads/tracked/TrackedDataResponse.java +++ b/src/java/org/apache/cassandra/service/reads/tracked/TrackedDataResponse.java @@ -18,7 +18,9 @@ package org.apache.cassandra.service.reads.tracked; +import org.apache.cassandra.db.IReadResponse; import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.db.ReadKind; import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.filter.ColumnFilter; import org.apache.cassandra.db.filter.DataLimits; @@ -40,7 +42,7 @@ import java.util.List; import com.google.common.base.Preconditions; -public class TrackedDataResponse +public class TrackedDataResponse implements IReadResponse { private final int serializationVersion; private final List data; @@ -169,4 +171,10 @@ public class TrackedDataResponse return size; } }; + + @Override + public ReadKind kind() + { + return ReadKind.TRACKED_DATA; + } } diff --git a/src/java/org/apache/cassandra/service/reads/tracked/TrackedRead.java b/src/java/org/apache/cassandra/service/reads/tracked/TrackedRead.java index 6a10357cf8..261f7aef6d 100644 --- a/src/java/org/apache/cassandra/service/reads/tracked/TrackedRead.java +++ b/src/java/org/apache/cassandra/service/reads/tracked/TrackedRead.java @@ -48,17 +48,22 @@ import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.locator.*; import org.apache.cassandra.net.*; import org.apache.cassandra.replication.MutationTrackingService; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.reads.ReadCoordinator; import org.apache.cassandra.service.reads.SpeculativeRetryPolicy; import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Dispatcher; +import org.apache.cassandra.transport.Dispatcher.RequestTime; import org.apache.cassandra.utils.Clock; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.concurrent.AsyncPromise; import org.apache.cassandra.utils.concurrent.Future; +import org.apache.cassandra.utils.concurrent.ImmediateFuture; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static org.apache.cassandra.db.ReadKind.TRACKED_DATA; +import static org.apache.cassandra.db.ReadKind.TRACKED_SUMMARY; import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.readMetrics; public abstract class TrackedRead, P extends ReplicaPlan.ForRead> implements RequestCallback @@ -419,7 +424,7 @@ public abstract class TrackedRead, P extends ReplicaPlan. }; } - public abstract static class Request + public abstract static class Request implements EmbeddableSinglePartitionReadCommand { public final Id readId; public final ReadCommand command; @@ -434,7 +439,23 @@ public abstract class TrackedRead, P extends ReplicaPlan. this.summaryNodes = summaryNodes; } + @Override + public TableMetadata metadata() + { + return command.metadata(); + } + + @Override + public DecoratedKey partitionKey() + { + // The command could be a PartitionRangeRead in which case nothing should call partitionKey + // If something does it will generate a ClassCastException which is an acceptable way to signal the error + return ((SinglePartitionReadCommand)command).partitionKey(); + } + public abstract void executeLocally(Message message, ClusterMetadata metadata); + + public abstract Future executeLocally(Request request, ClusterMetadata metadata, RequestTime requestTime); } public static class DataRequest extends Request @@ -450,6 +471,7 @@ public abstract class TrackedRead, P extends ReplicaPlan. @Override public void executeLocally(Message message, ClusterMetadata metadata) { + // TODO This is 1000% the wrong deadline? Dispatcher.RequestTime requestTime = new Dispatcher.RequestTime(message.createdAtNanos()); AsyncPromise promise = MutationTrackingService.instance @@ -466,6 +488,14 @@ public abstract class TrackedRead, P extends ReplicaPlan. }); } + @Override + public Future executeLocally(Request request, ClusterMetadata metadata, RequestTime requestTime) + { + return MutationTrackingService.instance + .localReads() + .beginRead(readId, metadata, command, consistencyLevel, summaryNodes, requestTime, TrackedLocalReads.Completer.DEFAULT); + } + public static final IVersionedSerializer serializer = new IVersionedSerializer<>() { @Override @@ -504,9 +534,15 @@ public abstract class TrackedRead, P extends ReplicaPlan. TypeSizes.sizeof(request.consistencyLevel.code); } }; + + @Override + public ReadKind kind() + { + return TRACKED_DATA; + } } - public static class SummaryRequest extends Request + public static class SummaryRequest extends Request implements EmbeddableSinglePartitionReadCommand { public SummaryRequest(Id readId, ReadCommand command, int dataNode, int[] summaryNodes) { @@ -519,6 +555,13 @@ public abstract class TrackedRead, P extends ReplicaPlan. ReadReconciliations.instance.handleSummaryRequest((SummaryRequest) message.payload); } + @Override + public Future executeLocally(Request request, ClusterMetadata metadata, RequestTime requestTime) + { + ReadReconciliations.instance.handleSummaryRequest((SummaryRequest) request); + return ImmediateFuture.success(null); + } + public static final IVersionedSerializer serializer = new IVersionedSerializer<>() { @Override @@ -554,6 +597,12 @@ public abstract class TrackedRead, P extends ReplicaPlan. ((long) TypeSizes.INT_SIZE * request.summaryNodes.length); } }; + + @Override + public ReadKind kind() + { + return TRACKED_SUMMARY; + } } public static final IVerbHandler verbHandler = new AbstractReadCommandVerbHandler<>() diff --git a/src/java/org/apache/cassandra/service/reads/tracked/TrackedSummaryResponse.java b/src/java/org/apache/cassandra/service/reads/tracked/TrackedSummaryResponse.java index ac6a4ddbec..ba48d84f3d 100644 --- a/src/java/org/apache/cassandra/service/reads/tracked/TrackedSummaryResponse.java +++ b/src/java/org/apache/cassandra/service/reads/tracked/TrackedSummaryResponse.java @@ -17,6 +17,8 @@ */ package org.apache.cassandra.service.reads.tracked; +import org.apache.cassandra.db.IReadResponse; +import org.apache.cassandra.db.ReadKind; import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.util.DataInputPlus; @@ -29,7 +31,9 @@ import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class TrackedSummaryResponse +import static org.apache.cassandra.db.ReadKind.TRACKED_SUMMARY; + +public class TrackedSummaryResponse implements IReadResponse { private static final Logger logger = LoggerFactory.getLogger(TrackedSummaryResponse.class); @@ -89,4 +93,10 @@ public class TrackedSummaryResponse TypeSizes.INT_SIZE * (long) summary.summaryNodes.length; } }; + + @Override + public ReadKind kind() + { + return TRACKED_SUMMARY; + } } diff --git a/src/java/org/apache/cassandra/triggers/TriggerDisabledException.java b/src/java/org/apache/cassandra/triggers/TriggerDisabledException.java index 10ec957d28..050762615f 100644 --- a/src/java/org/apache/cassandra/triggers/TriggerDisabledException.java +++ b/src/java/org/apache/cassandra/triggers/TriggerDisabledException.java @@ -18,6 +18,7 @@ package org.apache.cassandra.triggers; +import org.apache.cassandra.exceptions.CassandraExceptionCode; import org.apache.cassandra.exceptions.InvalidRequestException; public class TriggerDisabledException extends InvalidRequestException @@ -26,4 +27,10 @@ public class TriggerDisabledException extends InvalidRequestException { super(message); } + + @Override + public CassandraExceptionCode getCassandraExceptionCode() + { + return CassandraExceptionCode.TRIGGER_DISABLED; + } } diff --git a/src/java/org/apache/cassandra/utils/CollectionSerializers.java b/src/java/org/apache/cassandra/utils/CollectionSerializers.java index 64209cc901..b7e5c0a1ad 100644 --- a/src/java/org/apache/cassandra/utils/CollectionSerializers.java +++ b/src/java/org/apache/cassandra/utils/CollectionSerializers.java @@ -55,6 +55,30 @@ import static org.apache.cassandra.db.TypeSizes.sizeofUnsignedVInt; public class CollectionSerializers { + /** + * A simple UTF-8 string serializer for use with collection serialization methods. + */ + public static final IVersionedSerializer STRING_SERIALIZER = new IVersionedSerializer<>() + { + @Override + public void serialize(String str, DataOutputPlus out, int version) throws IOException + { + out.writeUTF(str); + } + + @Override + public String deserialize(DataInputPlus in, int version) throws IOException + { + return in.readUTF(); + } + + @Override + public long serializedSize(String str, int version) + { + return org.apache.cassandra.db.TypeSizes.sizeof(str); + } + }; + public static void serializeCollection(Collection values, DataOutputPlus out, UnversionedSerializer valueSerializer) throws IOException { out.writeUnsignedVInt32(values.size()); diff --git a/test/distributed/org/apache/cassandra/distributed/test/PaxosRepair2Test.java b/test/distributed/org/apache/cassandra/distributed/test/PaxosRepair2Test.java index d6e61161c3..506eb5e2b2 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/PaxosRepair2Test.java +++ b/test/distributed/org/apache/cassandra/distributed/test/PaxosRepair2Test.java @@ -438,7 +438,7 @@ public class PaxosRepair2Test extends TestBaseImpl private static int ballotDeletion(Commit commit) { - return (int) TimeUnit.MICROSECONDS.toSeconds(commit.ballot.unixMicros()) + SystemKeyspace.legacyPaxosTtlSec(commit.update.metadata()); + return (int) TimeUnit.MICROSECONDS.toSeconds(commit.ballot.unixMicros()) + SystemKeyspace.legacyPaxosTtlSec(commit.metadata()); } private static void backdateTimestamps(int seconds) diff --git a/test/distributed/org/apache/cassandra/distributed/test/ShortReadProtectionTest.java b/test/distributed/org/apache/cassandra/distributed/test/ShortReadProtectionTestBase.java similarity index 97% rename from test/distributed/org/apache/cassandra/distributed/test/ShortReadProtectionTest.java rename to test/distributed/org/apache/cassandra/distributed/test/ShortReadProtectionTestBase.java index c3ec8a68cc..3047cf3f00 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/ShortReadProtectionTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/ShortReadProtectionTestBase.java @@ -35,7 +35,6 @@ import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.apache.cassandra.dht.Murmur3Partitioner; @@ -55,11 +54,10 @@ import static org.apache.cassandra.distributed.api.ConsistencyLevel.SERIAL; import static org.apache.cassandra.distributed.shared.AssertUtils.row; /** - * Tests short read protection, the mechanism that ensures distributed queries at read consistency levels > ONE/LOCAL_ONE + * Base class for testing short read protection, the mechanism that ensures distributed queries at read consistency levels > ONE/LOCAL_ONE * avoid short reads that might happen when a limit is used and reconciliation accepts less rows than such limit. */ -@RunWith(Parameterized.class) -public class ShortReadProtectionTest extends TestBaseImpl +public abstract class ShortReadProtectionTestBase extends TestBaseImpl { private static final int NUM_NODES = 3; private static final int[] PAGE_SIZES = new int[]{ 1, 10 }; @@ -89,22 +87,23 @@ public class ShortReadProtectionTest extends TestBaseImpl @Parameterized.Parameter(3) public TransactionalMode transactionalMode; - @Parameterized.Parameter(4) - public ReplicationType replicationType; - - @Parameterized.Parameters(name = "{index}: read_cl={0} flush={1} paging={2}, transactionalMode={3}, replication={4}") + @Parameterized.Parameters(name = "{index}: read_cl={0} flush={1} paging={2}, transactionalMode={3}") public static Collection data() { List result = new ArrayList<>(); 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) - for (ReplicationType replication : ReplicationType.values()) - result.add(new Object[]{ readConsistencyLevel, flush, paging, mode, replication}); + for (boolean paging : BOOLEANS) + result.add(new Object[]{ readConsistencyLevel, flush, paging, mode}); return result; } + /** + * Returns the replication type for this test instance. + */ + protected abstract ReplicationType getReplicationType(); + @BeforeClass public static void setupCluster() throws IOException { @@ -129,7 +128,7 @@ public class ShortReadProtectionTest extends TestBaseImpl @Before public void setupTester() { - tester = new Tester(readConsistencyLevel, flush, paging, transactionalMode, replicationType); + tester = new Tester(readConsistencyLevel, flush, paging, transactionalMode, getReplicationType()); } @After @@ -583,4 +582,4 @@ public class ShortReadProtectionTest extends TestBaseImpl cluster.schemaChange(format("DROP TABLE IF EXISTS %s")); } } -} +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/test/TrackedReplicationShortReadProtectionTest.java b/test/distributed/org/apache/cassandra/distributed/test/TrackedReplicationShortReadProtectionTest.java new file mode 100644 index 0000000000..694a74c96c --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/TrackedReplicationShortReadProtectionTest.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.apache.cassandra.schema.ReplicationType; + +/** + * Tests short read protection with tracked replication. + */ +@RunWith(Parameterized.class) +public class TrackedReplicationShortReadProtectionTest extends ShortReadProtectionTestBase +{ + @Override + protected ReplicationType getReplicationType() + { + return ReplicationType.tracked; + } +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/test/TransientRangeMovement2Test.java b/test/distributed/org/apache/cassandra/distributed/test/TransientRangeMovement2Test.java index 29926b36fe..1fd7636b10 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/TransientRangeMovement2Test.java +++ b/test/distributed/org/apache/cassandra/distributed/test/TransientRangeMovement2Test.java @@ -57,6 +57,7 @@ public class TransientRangeMovement2Test extends TestBaseImpl .withTokenSupplier(new OPPTokens()) .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) .withConfig(conf -> conf.set("transient_replication_enabled","true") + .set("mutation_tracking_enabled", "true") .set("partitioner", "OrderPreservingPartitioner") // just makes it easier to read the tokens in the log .with(Feature.NETWORK, Feature.GOSSIP)) .start())) @@ -111,6 +112,7 @@ public class TransientRangeMovement2Test extends TestBaseImpl .withTokenSupplier(new OPPTokens()) .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) .withConfig(conf -> conf.set("transient_replication_enabled","true") + .set("mutation_tracking_enabled", "true") .set("partitioner", "OrderPreservingPartitioner") // just makes it easier to read the tokens in the log .set("hinted_handoff_enabled", "false") .with(Feature.NETWORK, Feature.GOSSIP)) @@ -160,6 +162,7 @@ public class TransientRangeMovement2Test extends TestBaseImpl .withTokenSupplier(new OPPTokens()) .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) .withConfig(conf -> conf.set("transient_replication_enabled","true") + .set("mutation_tracking_enabled", "true") .set("partitioner", "OrderPreservingPartitioner") .set("hinted_handoff_enabled", "false") .with(Feature.NETWORK, Feature.GOSSIP)) diff --git a/test/distributed/org/apache/cassandra/distributed/test/TransientRangeMovementTest.java b/test/distributed/org/apache/cassandra/distributed/test/TransientRangeMovementTest.java index 45996358bd..059e86e382 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/TransientRangeMovementTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/TransientRangeMovementTest.java @@ -73,6 +73,7 @@ public class TransientRangeMovementTest extends TestBaseImpl .withTokenSupplier(new OPPTokens()) .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) .withConfig(conf -> conf.set("transient_replication_enabled","true") + .set("mutation_tracking_enabled", "true") .set("partitioner", "OrderPreservingPartitioner") .set("hinted_handoff_enabled", "false") .with(Feature.NETWORK, Feature.GOSSIP)) @@ -110,6 +111,7 @@ public class TransientRangeMovementTest extends TestBaseImpl .withTokenSupplier(new OPPTokensReplace()) .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) .withConfig(conf -> conf.set("transient_replication_enabled","true") + .set("mutation_tracking_enabled", "true") .set("partitioner", "OrderPreservingPartitioner") .with(Feature.NETWORK, Feature.GOSSIP)) .start())) @@ -171,6 +173,7 @@ public class TransientRangeMovementTest extends TestBaseImpl .withTokenSupplier(new OPPTokens()) .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0")) .withConfig(conf -> conf.set("transient_replication_enabled","true") + .set("mutation_tracking_enabled", "true") .set("partitioner", "OrderPreservingPartitioner") .set("hinted_handoff_enabled", "false") .with(Feature.NETWORK, Feature.GOSSIP)) @@ -287,7 +290,7 @@ public class TransientRangeMovementTest extends TestBaseImpl public static void populate(Cluster cluster) throws ExecutionException, InterruptedException { - cluster.schemaChange("create keyspace tr with replication = {'class':'NetworkTopologyStrategy', 'dc0':'3/1'}"); + cluster.schemaChange("create keyspace tr with replication = {'class':'NetworkTopologyStrategy', 'dc0':'3/1'} AND replication_type='tracked'"); cluster.schemaChange("create table tr.x (id varchar primary key) with read_repair = 'NONE'"); for (int i = 0; i < 50; i++) cluster.coordinator(1).execute("insert into tr.x (id) values (?)", ConsistencyLevel.QUORUM, toStr(i)); diff --git a/test/distributed/org/apache/cassandra/distributed/test/UntrackedReplicationShortReadProtectionTest.java b/test/distributed/org/apache/cassandra/distributed/test/UntrackedReplicationShortReadProtectionTest.java new file mode 100644 index 0000000000..9bf82ee681 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/UntrackedReplicationShortReadProtectionTest.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.apache.cassandra.schema.ReplicationType; + +/** + * Tests short read protection with untracked replication. + */ +@RunWith(Parameterized.class) +public class UntrackedReplicationShortReadProtectionTest extends ShortReadProtectionTestBase +{ + @Override + protected ReplicationType getReplicationType() + { + return ReplicationType.untracked; + } +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/MetadataChangeSimulationTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/MetadataChangeSimulationTest.java index b13cab2660..e2e0ddcbcd 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/MetadataChangeSimulationTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/MetadataChangeSimulationTest.java @@ -89,6 +89,7 @@ public class MetadataChangeSimulationTest extends CMSTestBase { DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); DatabaseDescriptor.setTransientReplicationEnabledUnsafe(true); + DatabaseDescriptor.setMutationTrackingEnabled(true); } @Test diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/NTSSimulationTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/NTSSimulationTest.java index c0ce61226b..76498d69e5 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/NTSSimulationTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/NTSSimulationTest.java @@ -40,6 +40,7 @@ public class NTSSimulationTest extends CMSTestBase { DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); DatabaseDescriptor.setTransientReplicationEnabledUnsafe(true); + DatabaseDescriptor.setMutationTrackingEnabled(true); } @Before diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/OperationalEquivalenceTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/OperationalEquivalenceTest.java index 2f3e9f81ef..4e81b2afe7 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/OperationalEquivalenceTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/OperationalEquivalenceTest.java @@ -62,6 +62,7 @@ public class OperationalEquivalenceTest extends CMSTestBase static { DatabaseDescriptor.setTransientReplicationEnabledUnsafe(true); + DatabaseDescriptor.setMutationTrackingEnabled(true); } @Test diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/SimpleStrategySimulationTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/SimpleStrategySimulationTest.java index b8f98f68a0..c68674baab 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/SimpleStrategySimulationTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/SimpleStrategySimulationTest.java @@ -40,6 +40,7 @@ public class SimpleStrategySimulationTest extends CMSTestBase { DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); DatabaseDescriptor.setTransientReplicationEnabledUnsafe(true); + DatabaseDescriptor.setMutationTrackingEnabled(true); } @Before diff --git a/test/distributed/org/apache/cassandra/distributed/test/tracking/MutationTrackingCasForwardingTest.java b/test/distributed/org/apache/cassandra/distributed/test/tracking/MutationTrackingCasForwardingTest.java new file mode 100644 index 0000000000..4276067307 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/tracking/MutationTrackingCasForwardingTest.java @@ -0,0 +1,273 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test.tracking; + +import java.util.List; + +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.Util; +import org.apache.cassandra.db.ColumnFamilyStore; +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.test.TestBaseImpl; +import org.apache.cassandra.gms.Gossiper; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.replication.CoordinatorLogId; +import org.apache.cassandra.replication.MutationSummary; +import org.apache.cassandra.replication.MutationTrackingService; +import org.apache.cassandra.replication.Offsets; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.ReplicationType; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.service.StorageService; + +import static org.apache.cassandra.distributed.test.tracking.MutationTrackingUtils.getOnlyLogId; +import static org.apache.cassandra.distributed.test.tracking.MutationTrackingUtils.summaryIdSpace; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Establish that a coordinator for CAS is forwarding commit to a replica coordinator + */ +public class MutationTrackingCasForwardingTest extends TestBaseImpl +{ + private static final Logger logger = LoggerFactory.getLogger(MutationTrackingCasForwardingTest.class); + + private static final String CONDITIONAL_INSERT_CQL = "INSERT INTO " + KEYSPACE + ".tbl (k, v) VALUES (1, 1) IF NOT EXISTS"; + + @Test + public void testCasForwardingPaxosV1() throws Throwable + { + testCasForwarding("v1", false); // non-replica coordinator + } + + @Test + public void testCasForwardingPaxosV1ReplicaCoordinator() throws Throwable + { + testCasForwarding("v1", true); // replica coordinator + } + + @Test + public void testCasForwardingPaxosV2() throws Throwable + { + testCasForwarding("v2", false); // non-replica coordinator + } + + @Test + public void testCasForwardingPaxosV2ReplicaCoordinator() throws Throwable + { + testCasForwarding("v2", true); // replica coordinator + } + + private void testCasForwarding(String paxosVariant, boolean useReplicaCoordinator) throws Throwable + { + try (Cluster cluster = Cluster.build(4) + .withConfig(cfg -> cfg.with(Feature.NETWORK) + .with(Feature.GOSSIP) + .set("mutation_tracking_enabled", "true") + .set("transient_replication_enabled", "true") + .set("paxos_variant", paxosVariant)) + .start()) + { + String keyspaceName = KEYSPACE; + + // Create tracked keyspace with transient replicas + cluster.schemaChange(withKeyspace("CREATE KEYSPACE %s WITH replication = " + + "{'class': 'SimpleStrategy', 'replication_factor': '3/1'} " + + "AND replication_type='tracked';")); + + cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (k int primary key, v int);")); + + cluster.get(1).runOnInstance(() -> { + KeyspaceMetadata keyspace = Schema.instance.getKeyspaceMetadata(keyspaceName); + assertEquals(ReplicationType.tracked, keyspace.params.replicationType); + }); + + // Dynamically determine which nodes are replicas for key "1" + String replicaEndpointsStr = cluster.get(1).callOnInstance(() -> { + List endpoints = StorageService.instance.getNaturalEndpointsWithPort(keyspaceName, "tbl", "1"); + return String.join(",", endpoints); + }); + + // Parse replica endpoints and convert to node numbers + String[] replicaEndpoints = replicaEndpointsStr.split(","); + int[] replicaNodes = new int[replicaEndpoints.length]; + + for (int i = 0; i < replicaEndpoints.length; i++) { + String endpoint = replicaEndpoints[i]; + // Extract node number from address like "127.0.0.1:7000" or "/127.0.0.1:7000" -> 1 + int colonIndex = endpoint.indexOf(':'); + String hostPart = colonIndex > 0 ? endpoint.substring(0, colonIndex) : endpoint; + int lastDotIndex = hostPart.lastIndexOf('.'); + int nodeNum = Integer.parseInt(hostPart.substring(lastDotIndex + 1)); + replicaNodes[i] = nodeNum; + } + + // Determine coordinator based on test parameter + int coordinatorNode; + if (useReplicaCoordinator) { + // Use first replica node as coordinator + coordinatorNode = replicaNodes[0]; + logger.info("DEBUG testCasForwarding: Using replica coordinator: " + coordinatorNode); + } else { + // Find the non-replica node + coordinatorNode = -1; + for (int i = 1; i <= 4; i++) { + boolean isReplica = false; + for (int replicaNode : replicaNodes) { + if (i == replicaNode) { + isReplica = true; + break; + } + } + if (!isReplica) { + coordinatorNode = i; + break; + } + } + logger.info("DEBUG testCasForwarding: Using non-replica coordinator: " + coordinatorNode); + } + + // Find a replica node to block (not the same as the coordinator) + int blockedReplicaNode = -1; + for (int replicaNode : replicaNodes) { + if (replicaNode != coordinatorNode) { + blockedReplicaNode = replicaNode; + break; + } + } + + logger.info("DEBUG testCasForwarding: Blocked replica node: " + blockedReplicaNode); + + // Block the selected replica node from receiving Paxos commit messages + String blockedAddress = "127.0.0." + blockedReplicaNode; + cluster.filters().allVerbs().to(blockedReplicaNode).drop().on(); + cluster.filters().allVerbs().from(blockedReplicaNode).drop().on(); + + // Mark the blocked replica node as down in gossip so it doesn't participate in Paxos + for (int i = 1; i <= 4; i++) + { + if (i != blockedReplicaNode) + { + final String addressToBlock = blockedAddress; + cluster.get(i).runOnInstance(() -> Gossiper.instance.convict(InetAddressAndPort.getByNameUnchecked(addressToBlock), Double.MAX_VALUE)); + } + } + + // Perform CAS operation from determined coordinator + // This should trigger forwarding if coordinator is not a replica + cluster.coordinator(coordinatorNode).execute(CONDITIONAL_INSERT_CQL, ConsistencyLevel.SERIAL, ConsistencyLevel.QUORUM); + + // Verify that unblocked replica nodes have the mutation tracked + for (int replicaNode : replicaNodes) { + if (replicaNode != blockedReplicaNode) { + final int nodeNum = replicaNode; + cluster.get(nodeNum).runOnInstance(() -> { + MutationSummary summary = MutationTrackingService.instance.createSummaryForKey( + Util.dk(1), + ColumnFamilyStore.getIfExists(keyspaceName, "tbl").metadata.id, + true); + assertEquals("Node " + nodeNum + " should have mutation tracked", 1, summary.size()); + + CoordinatorLogId logId = getOnlyLogId(summary); + Offsets summaryIds = summaryIdSpace(summary.get(logId)); + assertEquals("Should have exactly one mutation ID tracked", 1, summaryIds.offsetCount()); + }); + } + } + + // The blocked replica node should not have the mutation yet due to filtering + cluster.get(blockedReplicaNode).runOnInstance(() -> { + MutationSummary summary = MutationTrackingService.instance.createSummaryForKey( + Util.dk(1), + ColumnFamilyStore.getIfExists(keyspaceName, "tbl").metadata.id, + true); + assertEquals("Blocked replica node should not have mutation yet", 0, summary.size()); + }); + + // Clear filters and revive the blocked replica node + cluster.filters().reset(); + final String revivedAddress = "127.0.0." + blockedReplicaNode; + for (int i = 1; i <= 4; i++) + { + if (i != blockedReplicaNode) + { + cluster.get(i).runOnInstance(() -> { + InetAddressAndPort endpoint = InetAddressAndPort.getByNameUnchecked(revivedAddress); + Gossiper.runInGossipStageBlocking(() -> Gossiper.instance.realMarkAlive(endpoint, Gossiper.instance.getEndpointStateForEndpoint(endpoint))); + }); + } + } + + // Perform a non-SERIAL read from coordinator to trigger mutation tracking propagation + String selectCql = withKeyspace("SELECT * FROM %s.tbl WHERE k = 1"); + Object[][] result = cluster.coordinator(coordinatorNode).execute(selectCql, ConsistencyLevel.ALL); + assertEquals("Should find the inserted row", 1, result.length); + assertEquals("Key should be 1", 1, result[0][0]); + assertEquals("Value should be 1", 1, result[0][1]); + + // Now the blocked replica node should have the mutation propagated via mutation tracking + // Note: When using replica coordinator, propagation behavior may differ + cluster.get(blockedReplicaNode).runOnInstance(() -> { + MutationSummary summary = MutationTrackingService.instance.createSummaryForKey( + Util.dk(1), + ColumnFamilyStore.getIfExists(keyspaceName, "tbl").metadata.id, + true); + if (useReplicaCoordinator) { + // For replica coordinator, propagation may not happen the same way + // We expect either 0 (not propagated) or 1 (propagated) + assertTrue("Blocked replica node should have 0 or 1 mutations", summary.size() <= 1); + } else { + // For non-replica coordinator, propagation should happen + assertEquals("Blocked replica node should now have mutation propagated", 1, summary.size()); + } + }); + + // Verify all replicas have consistent mutation tracking and same mutation ID + String[] mutationIds = new String[replicaNodes.length]; + for (int i = 0; i < replicaNodes.length; i++) { + final int nodeNum = replicaNodes[i]; + final int arrayIndex = i; + cluster.get(nodeNum).runOnInstance(() -> { + MutationSummary summary = MutationTrackingService.instance.createSummaryForKey( + Util.dk(1), + ColumnFamilyStore.getIfExists(keyspaceName, "tbl").metadata.id, + true); + assertEquals("All replicas should have consistent tracking", 1, summary.size()); + + CoordinatorLogId logId = getOnlyLogId(summary); + Offsets summaryIds = summaryIdSpace(summary.get(logId)); + assertEquals("Should have exactly one mutation ID tracked", 1, summaryIds.offsetCount()); + + // Store the mutation ID for comparison + mutationIds[arrayIndex] = summaryIds.toString(); + }); + } + + // Verify all replicas have the same mutation ID + for (int i = 1; i < mutationIds.length; i++) { + assertEquals("All replicas should have same mutation ID", mutationIds[0], mutationIds[i]); + } + } + } +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/test/tracking/MutationTrackingCommitAndPrepareTest.java b/test/distributed/org/apache/cassandra/distributed/test/tracking/MutationTrackingCommitAndPrepareTest.java new file mode 100644 index 0000000000..4b4e81cf20 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/tracking/MutationTrackingCommitAndPrepareTest.java @@ -0,0 +1,298 @@ +/* + * 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.tracking; + +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.Util; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.dht.Murmur3Partitioner; +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.test.TestBaseImpl; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.replication.CoordinatorLogId; +import org.apache.cassandra.replication.MutationSummary; +import org.apache.cassandra.replication.MutationTrackingService; +import org.apache.cassandra.replication.Offsets; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.ReplicationType; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.ByteBufferUtil; + +import static org.apache.cassandra.distributed.test.tracking.MutationTrackingUtils.getOnlyLogId; +import static org.apache.cassandra.distributed.test.tracking.MutationTrackingUtils.summaryIdSpace; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Tests for PaxosCommitAndPrepare with mutation tracking enabled. + * + * When mutation tracking is enabled, PaxosCommitAndPrepare takes a different path: + * instead of embedding the commit in the prepare message, it performs a synchronous + * commit followed by a separate prepare. This test verifies that mutation tracking + * works correctly in this scenario. + */ +public class MutationTrackingCommitAndPrepareTest extends TestBaseImpl +{ + private static final Logger logger = LoggerFactory.getLogger(MutationTrackingCommitAndPrepareTest.class); + + private static final String INSERT_CQL = "INSERT INTO " + KEYSPACE + ".tbl (k, v) VALUES (1, 1)"; + private static final String INSERT_CQL_2 = "INSERT INTO " + KEYSPACE + ".tbl (k, v) VALUES (1, 2)"; + private static final String CONDITIONAL_INSERT_CQL = INSERT_CQL + " IF NOT EXISTS"; + + /** + * Test 1: Basic CommitAndPrepare path with mutation tracking. + * + * This test forces the CommitAndPrepare path by blocking the commit phase of the first CAS, + * leaving an uncommitted ballot. The second CAS will then find this uncommitted ballot + * during prepare and trigger CommitAndPrepare. With mutation tracking enabled, this should + * take the sequential commit-then-prepare path. + */ + @Test + public void testCommitAndPrepareBasicPath() throws Throwable + { + try (Cluster cluster = Cluster.build(3) + .withConfig(cfg -> cfg.with(Feature.NETWORK) + .with(Feature.GOSSIP) + .set("mutation_tracking_enabled", "true") + .set("paxos_variant", "v2_without_linearizable_reads")) + .start()) + { + String keyspaceName = KEYSPACE; + + // Create tracked keyspace with RF=3 + cluster.schemaChange(withKeyspace("CREATE KEYSPACE %s WITH replication = " + + "{'class': 'SimpleStrategy', 'replication_factor': 3} " + + "AND replication_type='tracked';")); + + cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (k int primary key, v int);")); + + // Verify keyspace is tracked + cluster.get(1).runOnInstance(() -> + { + KeyspaceMetadata keyspace = Schema.instance.getKeyspaceMetadata(keyspaceName); + assertEquals(ReplicationType.tracked, keyspace.params.replicationType); + logger.info("Verified keyspace {} is tracked", keyspaceName); + }); + + // Strategy: Block PAXOS_COMMIT_REQ on the first CAS to prevent commit completion, + // leaving an uncommitted ballot. The second CAS will then trigger CommitAndPrepare. + // This follows the same pattern as PaxosRepairTest. + cluster.filters().verbs(Verb.PAXOS_COMMIT_REQ.id).drop(); + + logger.info("Executing first CAS to create uncommitted ballot"); + // First CAS - will complete prepare and propose but not commit (due to filter) + // This will leave an uncommitted accepted proposal + try + { + cluster.coordinator(1).execute(CONDITIONAL_INSERT_CQL, + ConsistencyLevel.SERIAL, + ConsistencyLevel.QUORUM); + logger.info("First CAS completed (may have timed out on commit)"); + } + catch (Exception e) + { + logger.info("First CAS threw exception (expected): {}", e.getMessage()); + // Expected - commit will timeout due to dropped messages + } + + // Reset filters so the next CAS can proceed normally + cluster.filters().reset(); + logger.info("Reset filters"); + + logger.info("Executing second CAS - should trigger CommitAndPrepare"); + // Second CAS - should find the uncommitted ballot and trigger CommitAndPrepare + Object[][] result = cluster.coordinator(1).execute(INSERT_CQL_2 + " IF NOT EXISTS", + ConsistencyLevel.SERIAL, + ConsistencyLevel.QUORUM); + + // CAS should succeed + assertTrue("Second CAS operation should succeed", result.length > 0); + logger.info("Second CAS succeeded"); + + // Verify mutation tracking shows correct state on coordinator (node 1) + cluster.get(1).runOnInstance(() -> + { + TableMetadata table = Schema.instance.getTableMetadata(keyspaceName, "tbl"); + DecoratedKey dk = Murmur3Partitioner.instance.decorateKey(ByteBufferUtil.bytes(1)); + MutationSummary summary = MutationTrackingService.instance.createSummaryForKey(dk, table.id, false); + + logger.info("Node 1 mutation summary: {} coordinator logs", summary.size()); + + // Should have at least one coordinator log entry + assertTrue("Should have at least one coordinator log", summary.size() >= 1); + + CoordinatorLogId logId = getOnlyLogId(summary); + Offsets offsets = summaryIdSpace(summary.get(logId)); + + logger.info("Node 1 has {} mutation offsets", offsets.offsetCount()); + // Should have mutations from the CommitAndPrepare + assertTrue("Should have at least one mutation offset", offsets.offsetCount() >= 1); + }); + + // Wait for reconciliation + // TODO: Replace Thread.sleep with Awaitility for condition-based waiting + Thread.sleep(2000); + + // Verify all nodes eventually see the mutation + for (int i = 1; i <= 3; i++) + { + int nodeId = i; + cluster.get(nodeId).runOnInstance(() -> + { + TableMetadata table = Schema.instance.getTableMetadata(keyspaceName, "tbl"); + DecoratedKey dk = Murmur3Partitioner.instance.decorateKey(ByteBufferUtil.bytes(1)); + MutationSummary summary = MutationTrackingService.instance.createSummaryForKey(dk, table.id, true); + + logger.info("Node {} mutation summary size: {}", nodeId, summary.size()); + // All nodes should eventually have the mutation + assertTrue("Node " + nodeId + " should have mutation in summary", summary.size() > 0); + }); + } + + // Verify data is readable + result = cluster.coordinator(1).execute("SELECT * FROM " + KEYSPACE + ".tbl WHERE k = 1", + ConsistencyLevel.QUORUM); + assertEquals("Should have one row", 1, result.length); + assertEquals("Key should be 1", 1, result[0][0]); + logger.info("Final value: {}", result[0][1]); + } + } + + /** + * Test 2: CommitAndPrepare with witness (transient) replicas. + * + * This test verifies that with witness replicas (transient replication), the commit + * in CommitAndPrepare only goes to full replicas, and the witness eventually gets + * the mutation through reconciliation. + */ + @Test + public void testCommitAndPrepareWithWitnessReplicas() throws Throwable + { + try (Cluster cluster = Cluster.build(3) + .withConfig(cfg -> cfg.with(Feature.NETWORK) + .with(Feature.GOSSIP) + .set("mutation_tracking_enabled", "true") + .set("transient_replication_enabled", "true") + .set("paxos_variant", "v2_without_linearizable_reads")) + .start()) + { + String keyspaceName = KEYSPACE; + + // Create tracked keyspace with RF='3/1' (2 full replicas + 1 witness/transient) + cluster.schemaChange(withKeyspace("CREATE KEYSPACE %s WITH replication = " + + "{'class': 'SimpleStrategy', 'replication_factor': '3/1'} " + + "AND replication_type='tracked';")); + + cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (k int primary key, v int);")); + + // Verify keyspace configuration + cluster.get(1).runOnInstance(() -> + { + KeyspaceMetadata keyspace = Schema.instance.getKeyspaceMetadata(keyspaceName); + assertEquals(ReplicationType.tracked, keyspace.params.replicationType); + logger.info("Verified keyspace {} is tracked with transient replication", keyspaceName); + }); + + // Similar strategy: block PAXOS_COMMIT_REQ to create uncommitted ballot + cluster.filters().verbs(Verb.PAXOS_COMMIT_REQ.id).drop(); + + logger.info("Executing first CAS to create uncommitted ballot"); + // First CAS - will complete prepare and propose but timeout on commit + try + { + cluster.coordinator(1).execute(CONDITIONAL_INSERT_CQL, + ConsistencyLevel.SERIAL, + ConsistencyLevel.QUORUM); + logger.info("First CAS completed (may have timed out)"); + } + catch (Exception e) + { + logger.info("First CAS threw exception (expected): {}", e.getMessage()); + // Expected - commit will timeout + } + + // Wait for async operations + // TODO: Replace Thread.sleep with Awaitility for condition-based waiting + Thread.sleep(1000); + + // Reset filters + cluster.filters().reset(); + logger.info("Reset filters"); + + logger.info("Executing second CAS - should trigger CommitAndPrepare"); + // Second CAS should find uncommitted ballot and trigger CommitAndPrepare + Object[][] result = cluster.coordinator(1).execute(INSERT_CQL_2 + " IF NOT EXISTS", + ConsistencyLevel.SERIAL, + ConsistencyLevel.QUORUM); + + assertTrue("Second CAS should succeed", result.length > 0); + logger.info("Second CAS succeeded"); + + // Verify mutation tracking on full replicas + // With RF=3/1, we have 2 full replicas and 1 transient + // The commit should only go to full replicas + for (int i = 1; i <= 3; i++) + { + int nodeId = i; + cluster.get(nodeId).runOnInstance(() -> + { + MutationSummary summary = MutationTrackingService.instance.createSummaryForKey( + Util.dk(1), + ColumnFamilyStore.getIfExists(keyspaceName, "tbl").metadata.id, + true); + logger.info("Node {} mutation summary size: {}", nodeId, summary.size()); + // All nodes should eventually have mutation tracking info (including witness through reconciliation) + assertTrue("Node " + nodeId + " should have mutation info", summary.size() > 0); + }); + } + + // Wait for reconciliation + // TODO: Replace Thread.sleep with Awaitility for condition-based waiting + Thread.sleep(2000); + + // Verify data consistency across all nodes (including witness) + // The data should be on full replicas, witness may not have it depending on when we check + int rowsFound = 0; + String selectCql = withKeyspace("SELECT * FROM %s.tbl WHERE k = 1"); + for (int i = 1; i <= 3; i++) + { + Object[][] nodeResult = cluster.get(i).executeInternal(selectCql); + logger.info("Node {} has {} rows", i, nodeResult.length); + assertTrue("Each node should have 0-1 rows", nodeResult.length == 0 || nodeResult.length == 1); + rowsFound += nodeResult.length; + } + logger.info("Total rows found across nodes: {}", rowsFound); + // At least 2 full replicas should have the row + assertTrue("At least 2 instances should have the row", rowsFound >= 2); + + // Verify data is readable with QUORUM (which requires full replicas) + result = cluster.coordinator(1).execute(selectCql, ConsistencyLevel.QUORUM); + assertEquals("Should have one row", 1, result.length); + assertEquals("Key should be 1", 1, result[0][0]); + logger.info("Final value from QUORUM read: {}", result[0][1]); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/tracking/MutationTrackingTest.java b/test/distributed/org/apache/cassandra/distributed/test/tracking/MutationTrackingTest.java index 94b1b489aa..b160458cd9 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/tracking/MutationTrackingTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/tracking/MutationTrackingTest.java @@ -66,7 +66,6 @@ public class MutationTrackingTest extends TestBaseImpl + " %s%n" + "APPLY BATCH"; - @Ignore @Test public void testBasicWritePath() throws Throwable { @@ -103,14 +102,12 @@ public class MutationTrackingTest extends TestBaseImpl } } - @Ignore @Test public void testWitnessPaxosV1Reads() throws Throwable { testWitnessPaxosReads("v1"); } - @Ignore @Test public void testWitnessPaxosV2Reads() throws Throwable { @@ -273,18 +270,16 @@ public class MutationTrackingTest extends TestBaseImpl } - @Ignore @Test public void testWitnessSerialPaxosV1WritesSkipped() throws Throwable { testWitnessWrites(CONDITIONAL_INSERT_CQL, ConsistencyLevel.SERIAL, "v1"); } - @Ignore @Test public void testWitnessSerialPaxosV2WritesSkipped() throws Throwable { - testWitnessWrites(CONDITIONAL_INSERT_CQL, ConsistencyLevel.SERIAL, "v1"); + testWitnessWrites(CONDITIONAL_INSERT_CQL, ConsistencyLevel.SERIAL, "v2"); } @Test @@ -358,71 +353,6 @@ public class MutationTrackingTest extends TestBaseImpl } } - @Test - public void testWitnessWriteSkippedPath() throws Throwable - { - try (Cluster cluster = Cluster.build(3) - .withConfig(cfg -> cfg.with(Feature.NETWORK) - .with(Feature.GOSSIP) - .set("mutation_tracking_enabled", "true") - .set("transient_replication_enabled", "true")) - .start()) - { - - cluster.schemaChange(withKeyspace("CREATE KEYSPACE %s WITH replication = " + - "{'class': 'SimpleStrategy', 'replication_factor': '3/1'} " + - "AND replication_type='tracked';")); - - cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (k int primary key, v int);")); - - String keyspaceName = KEYSPACE; - cluster.get(1).runOnInstance(() -> { - - KeyspaceMetadata keyspace = Schema.instance.getKeyspaceMetadata(keyspaceName); - assertEquals(ReplicationType.tracked, keyspace.params.replicationType); - }); - - cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.tbl (k, v) VALUES (1, 1)"), ConsistencyLevel.ALL); - - // Only two instances should have the row - int rowsFound = 0; - String singlePartitionSelectCQL = withKeyspace("SELECT * FROM %s.tbl WHERE k = 1"); - for (IInvokableInstance instance : cluster) - { - Object[][] result = instance.executeInternal(singlePartitionSelectCQL); - assertTrue("Each node should have 0-1 rows", result.length == 0 || result.length == 1); - rowsFound += result.length; - } - assertEquals("Only two instances should have the row", 2, rowsFound); - - cluster.get(1).runOnInstance(() -> { - TableMetadata table = Schema.instance.getTableMetadata(keyspaceName, "tbl"); - DecoratedKey dk = Murmur3Partitioner.instance.decorateKey(ByteBufferUtil.bytes(1)); - MutationSummary summary = MutationTrackingService.instance.createSummaryForKey(dk, table.id, false); - CoordinatorLogId logId = getOnlyLogId(summary); - - Offsets summaryIds = summaryIdSpace(summary.get(logId)); - assertEquals(1, summaryIds.offsetCount()); - }); - - Object[][] result = cluster.coordinator(1).execute(singlePartitionSelectCQL, ConsistencyLevel.ALL); - assertEquals(1, result.length); - String partitionRangeSelectCQL = withKeyspace("SELECT * FROM %s.tbl"); - result = cluster.coordinator(1).execute(partitionRangeSelectCQL, ConsistencyLevel.ALL); - assertEquals(1, result.length); - - // Read time reconciliation should not propagate the row to the witness node - rowsFound = 0; - for (IInvokableInstance instance : cluster) - { - result = instance.executeInternal(singlePartitionSelectCQL); - assertTrue("Each node should have 0-1 rows", result.length == 0 || result.length == 1); - rowsFound += result.length; - } - assertEquals("Only two instances should have the row", 2, rowsFound); - } - } - @Test public void testHintsNotWrittenOnFailedWrite() throws Throwable { diff --git a/test/harry/main/org/apache/cassandra/harry/model/TokenPlacementModel.java b/test/harry/main/org/apache/cassandra/harry/model/TokenPlacementModel.java index 353e292f22..daf8499647 100644 --- a/test/harry/main/org/apache/cassandra/harry/model/TokenPlacementModel.java +++ b/test/harry/main/org/apache/cassandra/harry/model/TokenPlacementModel.java @@ -410,7 +410,8 @@ public class TokenPlacementModel i++; } - return hasTransient ? KeyspaceParams.ntsTracked(args) : KeyspaceParams.nts(args); + ReplicationType replicationType = hasTransient ? ReplicationType.tracked : ReplicationType.untracked; + return KeyspaceParams.nts(replicationType, args); } private static Function> mapFunction(int dcs, int nodesPerDc, int transientsPerDc) @@ -619,7 +620,9 @@ public class TokenPlacementModel Map options = new HashMap<>(); options.put(ReplicationParams.CLASS, SimpleStrategy.class.getName()); options.put(SimpleStrategy.REPLICATION_FACTOR, dcReplicas().toString()); - ReplicationType replicationType = dcReplicas().transientCount > 0 ? ReplicationType.tracked : ReplicationType.untracked; + ReplicationType replicationType = dcReplicas().transientCount > 0 + ? ReplicationType.tracked + : ReplicationType.untracked; return KeyspaceParams.create(true, options, replicationType); } diff --git a/test/simulator/main/org/apache/cassandra/simulator/ClusterSimulation.java b/test/simulator/main/org/apache/cassandra/simulator/ClusterSimulation.java index 64cc4c8ef3..2134a1a954 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/ClusterSimulation.java +++ b/test/simulator/main/org/apache/cassandra/simulator/ClusterSimulation.java @@ -68,6 +68,7 @@ import org.apache.cassandra.io.util.FileSystems; import org.apache.cassandra.net.Verb; import org.apache.cassandra.service.consensus.TransactionalMode; import org.apache.cassandra.service.paxos.BallotGenerator; +import org.apache.cassandra.simulator.cluster.ReplicationConfig; import org.apache.cassandra.service.paxos.PaxosPrepare; import org.apache.cassandra.simulator.RandomSource.Choices; import org.apache.cassandra.simulator.asm.DeterministicChanceSupplier; @@ -224,6 +225,7 @@ public class ClusterSimulation implements AutoCloseable protected SimulatedTime.Listener timeListener = (i1, i2) -> {}; protected LongConsumer onThreadLocalRandomCheck; protected String transactionalMode = "off"; + protected ReplicationConfig replicationConfig = ReplicationConfig.SIMPLE; protected FutureActionSchedulerFactory futureActionSchedulerFactory; protected PerVerbFutureActionSchedulersFactory perVerbFutureActionSchedulersFactory; protected String memtableType = null; @@ -643,6 +645,23 @@ public class ClusterSimulation implements AutoCloseable return this; } + public Builder replicationConfig(ReplicationConfig config) + { + this.replicationConfig = config; + return this; + } + + public Builder replicationConfig(String config) + { + this.replicationConfig = ReplicationConfig.fromString(config); + return this; + } + + public ReplicationConfig replicationConfig() + { + return replicationConfig; + } + public abstract ClusterSimulation create(long seed) throws IOException; } @@ -902,6 +921,12 @@ public class ClusterSimulation implements AutoCloseable "default", Map.of("class_name", "SkipListMemtable")))); } + // Enable mutation tracking and transient replication for tracked modes + if (builder.replicationConfig.isTracked()) + config.set("mutation_tracking_enabled", "true"); + if (builder.replicationConfig.withWitnesses()) + config.set("transient_replication_enabled", "true"); + // TODO: Add remove() to IInstanceConfig if (config instanceof InstanceConfig) { diff --git a/test/simulator/main/org/apache/cassandra/simulator/SimulationRunner.java b/test/simulator/main/org/apache/cassandra/simulator/SimulationRunner.java index 8bca236f0d..b820acb375 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/SimulationRunner.java +++ b/test/simulator/main/org/apache/cassandra/simulator/SimulationRunner.java @@ -68,6 +68,7 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.CLOCK_MONO import static org.apache.cassandra.config.CassandraRelevantProperties.CONSISTENT_DIRECTORY_LISTINGS; import static org.apache.cassandra.config.CassandraRelevantProperties.DETERMINISM_SSTABLE_COMPRESSION_DEFAULT; import static org.apache.cassandra.config.CassandraRelevantProperties.DETERMINISM_UNSAFE_UUID_NODE; +import static org.apache.cassandra.config.CassandraRelevantProperties.DISABLE_CONSENSUS_REQUEST_FORWARDING; import static org.apache.cassandra.config.CassandraRelevantProperties.DISABLE_GOSSIP_ENDPOINT_REMOVAL; import static org.apache.cassandra.config.CassandraRelevantProperties.DISABLE_SSTABLE_ACTIVITY_TRACKING; import static org.apache.cassandra.config.CassandraRelevantProperties.DTEST_API_LOG_TOPOLOGY; @@ -273,6 +274,12 @@ public class SimulationRunner @Option(names = { "--transactional-mode" }, description = { "Starting transactional mode for the created table", "[off|mixed_reads|full]" }) protected String transactionalMode; + @Option(names = { "--replication-config" }, description = { "Replication configuration for the keyspace", "[simple|tracking|witnesses]" }) + protected String replicationConfig; + + @Option(names = { "--disable-consensus-forwarding" }, description = "Disable top-level CAS/consensus read forwarding") + protected boolean disableConsensusForwarding; + protected void propagate(B builder) { checkArgumentAllowed(debugRf, 0, 1, 2); @@ -349,6 +356,10 @@ public class SimulationRunner } Optional.ofNullable(transactionalMode).ifPresent(builder::transactionalMode); + Optional.ofNullable(replicationConfig).ifPresent(builder::replicationConfig); + + if (disableConsensusForwarding) + DISABLE_CONSENSUS_REQUEST_FORWARDING.setBoolean(true); } @Override diff --git a/test/simulator/main/org/apache/cassandra/simulator/SimulatorUtils.java b/test/simulator/main/org/apache/cassandra/simulator/SimulatorUtils.java index 1a76566f18..236ec15db8 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/SimulatorUtils.java +++ b/test/simulator/main/org/apache/cassandra/simulator/SimulatorUtils.java @@ -30,6 +30,7 @@ import java.util.stream.Collectors; import org.slf4j.Logger; import org.apache.cassandra.utils.concurrent.Threads; +import picocli.CommandLine; import io.netty.util.concurrent.FastThreadLocal; import picocli.CommandLine; diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/KeyspaceActions.java b/test/simulator/main/org/apache/cassandra/simulator/cluster/KeyspaceActions.java index 2f47f8cc1e..d8b965842e 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/KeyspaceActions.java +++ b/test/simulator/main/org/apache/cassandra/simulator/cluster/KeyspaceActions.java @@ -62,6 +62,7 @@ public class KeyspaceActions extends ClusterActions final String createTableCql; final ConsistencyLevel serialConsistency; final int[] primaryKeys; + final ReplicationConfig replicationConfig; final EnumSet topologyOps = EnumSet.noneOf(TopologyChange.class); final EnumSet consensusOps = EnumSet.noneOf(ConsensusChange.class); @@ -90,7 +91,8 @@ public class KeyspaceActions extends ClusterActions ConsistencyLevel serialConsistency, ClusterActionListener listener, int[] primaryKeys, - Debug debug) + Debug debug, + ReplicationConfig replicationConfig) { super(simulated, cluster, options, listener, debug); this.keyspace = keyspace; @@ -98,6 +100,7 @@ public class KeyspaceActions extends ClusterActions this.createTableCql = createTableCql; this.primaryKeys = primaryKeys; this.serialConsistency = serialConsistency; + this.replicationConfig = replicationConfig; this.nodeLookup = simulated.snitch; @@ -138,9 +141,12 @@ public class KeyspaceActions extends ClusterActions private String createKeyspaceCql(String keyspace) { String createKeyspaceCql = "CREATE KEYSPACE " + keyspace + " WITH replication = {'class': 'NetworkTopologyStrategy'"; + String[] formattedRfs = replicationConfig.formatReplicationFactorsPerDc(options.initialRf); for (int i = 0 ; i < options.initialRf.length ; ++i) - createKeyspaceCql += ", '" + snitch.nameOfDc(i) + "': " + options.initialRf[i]; - createKeyspaceCql += "};"; + createKeyspaceCql += ", '" + snitch.nameOfDc(i) + "': " + formattedRfs[i]; + createKeyspaceCql += "}"; + createKeyspaceCql += replicationConfig.asCqlKeyspaceClause(); + createKeyspaceCql += ";"; return createKeyspaceCql; } diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/ReplicationConfig.java b/test/simulator/main/org/apache/cassandra/simulator/cluster/ReplicationConfig.java new file mode 100644 index 0000000000..04855604b7 --- /dev/null +++ b/test/simulator/main/org/apache/cassandra/simulator/cluster/ReplicationConfig.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.simulator.cluster; + +import org.apache.cassandra.schema.ReplicationType; + +/** + * Configuration for replication behavior in simulations. + */ +public enum ReplicationConfig +{ + /** + * Standard untracked replication - no mutation tracking or witnesses. + */ + SIMPLE(ReplicationType.untracked, false), + + /** + * Mutation tracking enabled without witness replicas. + */ + TRACKING(ReplicationType.tracked, false), + + /** + * Mutation tracking enabled with witness replicas. + */ + WITNESSES(ReplicationType.tracked, true); + + private final ReplicationType replicationType; + private final boolean withWitnesses; + + ReplicationConfig(ReplicationType replicationType, boolean withWitnesses) + { + this.replicationType = replicationType; + this.withWitnesses = withWitnesses; + } + + public ReplicationType replicationType() + { + return replicationType; + } + + public boolean withWitnesses() + { + return withWitnesses; + } + + public boolean isTracked() + { + return replicationType.isTracked(); + } + + /** + * Calculates the formatted replication factors for each DC. + * For witnesses mode, uses format 'RF/transientCount' where transient replicas + * are distributed evenly across DCs while ensuring each DC has at least 1 full replica. + * + * @param rfPerDc array of RF per datacenter + * @return array of formatted strings for each DC (e.g., "'3/1'" for witnesses, "3" for others) + */ + public String[] formatReplicationFactorsPerDc(int[] rfPerDc) + { + if (!withWitnesses) + { + // No witnesses - just return RF as strings + String[] result = new String[rfPerDc.length]; + for (int i = 0; i < rfPerDc.length; i++) + result[i] = String.valueOf(rfPerDc[i]); + return result; + } + + // Calculate witness distribution using the multi-DC algorithm + int numDcs = rfPerDc.length; + int totalRf = 0; + for (int rf : rfPerDc) totalRf += rf; + + int totalFull = totalRf / 2 + 1; + int[] fullPerDc = new int[numDcs]; + + // Each DC gets at least 1 full replica + for (int i = 0; i < numDcs; i++) + fullPerDc[i] = 1; + + int remainingFull = totalFull - numDcs; + + // Distribute remaining full replicas to DCs with most remaining capacity + // Goal: spread witnesses (remaining slots) evenly + while (remainingFull > 0) + { + int maxWitnessesIdx = -1; + int maxWitnesses = 0; + for (int i = 0; i < numDcs; i++) + { + int witnesses = rfPerDc[i] - fullPerDc[i]; + if (witnesses > maxWitnesses) + { + maxWitnesses = witnesses; + maxWitnessesIdx = i; + } + } + if (maxWitnessesIdx >= 0 && maxWitnesses > 0) + { + fullPerDc[maxWitnessesIdx]++; + remainingFull--; + } + else + { + break; + } + } + + // Format as "'RF/witnesses'" + String[] result = new String[numDcs]; + for (int i = 0; i < numDcs; i++) + { + int witnesses = rfPerDc[i] - fullPerDc[i]; + result[i] = "'" + rfPerDc[i] + "/" + witnesses + "'"; + } + return result; + } + + /** + * Returns the CQL clause to append to CREATE KEYSPACE for this replication config. + * Returns empty string if no special configuration is needed. + * Note: Witness replicas are configured via the RF format (e.g., '3/1'), not via a keyspace property. + */ + public String asCqlKeyspaceClause() + { + if (replicationType == ReplicationType.untracked) + return ""; + + // Only append replication_type, witnesses are handled via RF format in formatReplicationFactorsPerDc + return " AND replication_type = '" + replicationType.name() + "'"; + } + + public static ReplicationConfig fromString(String name) + { + return valueOf(name.toUpperCase().replace('-', '_')); + } +} diff --git a/test/simulator/main/org/apache/cassandra/simulator/paxos/AbstractPairOfSequencesPaxosSimulation.java b/test/simulator/main/org/apache/cassandra/simulator/paxos/AbstractPairOfSequencesPaxosSimulation.java index 97114bee9f..33a17cdf77 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/paxos/AbstractPairOfSequencesPaxosSimulation.java +++ b/test/simulator/main/org/apache/cassandra/simulator/paxos/AbstractPairOfSequencesPaxosSimulation.java @@ -54,6 +54,7 @@ import org.apache.cassandra.simulator.RandomSource; import org.apache.cassandra.simulator.RunnableActionScheduler; import org.apache.cassandra.simulator.cluster.ClusterActions; import org.apache.cassandra.simulator.cluster.KeyspaceActions; +import org.apache.cassandra.simulator.cluster.ReplicationConfig; import org.apache.cassandra.simulator.systems.SimulatedActionTask; import org.apache.cassandra.simulator.systems.SimulatedSystems; import org.apache.cassandra.simulator.utils.IntRange; @@ -89,6 +90,7 @@ abstract class AbstractPairOfSequencesPaxosSimulation extends PaxosSimulation final AtomicInteger failedWrites = new AtomicInteger(); final long seed; final int[] primaryKeys; + final ReplicationConfig replicationConfig; public AbstractPairOfSequencesPaxosSimulation(SimulatedSystems simulated, Cluster cluster, @@ -97,7 +99,8 @@ abstract class AbstractPairOfSequencesPaxosSimulation extends PaxosSimulation int concurrency, IntRange simulateKeyForSeconds, IntRange withinKeyConcurrency, ConsistencyLevel serialConsistency, RunnableActionScheduler scheduler, Debug debug, long seed, int[] primaryKeys, - long runForNanos, LongSupplier jitter) + long runForNanos, LongSupplier jitter, + ReplicationConfig replicationConfig) { super(runForNanos < 0 ? STREAM_LIMITED : (clusterOptions.topologyChangeLimit <= 0 && clusterOptions.consensusChangeLimit <= 0) ? TIME_LIMITED : TIME_AND_STREAM_LIMITED, simulated, cluster, scheduler, runForNanos, jitter); @@ -111,6 +114,7 @@ abstract class AbstractPairOfSequencesPaxosSimulation extends PaxosSimulation this.seed = seed; this.primaryKeys = primaryKeys.clone(); Arrays.sort(this.primaryKeys); + this.replicationConfig = replicationConfig; } protected abstract String createTableStmt(); @@ -194,7 +198,7 @@ abstract class AbstractPairOfSequencesPaxosSimulation extends PaxosSimulation public ActionPlan plan() { ActionPlan plan = new KeyspaceActions(simulated, KEYSPACE, TABLE, createTableStmt(), cluster, - clusterOptions, serialConsistency, this, primaryKeys, debug).plan(joinAll()); + clusterOptions, serialConsistency, this, primaryKeys, debug, replicationConfig).plan(joinAll()); plan = plan.encapsulate(ActionPlan.setUpTearDown( ActionList.of( diff --git a/test/simulator/main/org/apache/cassandra/simulator/paxos/AccordClusterSimulation.java b/test/simulator/main/org/apache/cassandra/simulator/paxos/AccordClusterSimulation.java index be6939ffd9..df97271d86 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/paxos/AccordClusterSimulation.java +++ b/test/simulator/main/org/apache/cassandra/simulator/paxos/AccordClusterSimulation.java @@ -53,7 +53,7 @@ public class AccordClusterSimulation extends ClusterSimulation builder.readChance().select(random), builder.concurrency(), builder.primaryKeySeconds(), builder.withinKeyConcurrency(), SERIAL, schedulers, builder.debug(), seed, primaryKeys, builder.secondsToSimulate() >= 0 ? SECONDS.toNanos(builder.secondsToSimulate()) : -1, - () -> jitter.get(random)); + () -> jitter.get(random), builder.replicationConfig()); }); } diff --git a/test/simulator/main/org/apache/cassandra/simulator/paxos/Ballots.java b/test/simulator/main/org/apache/cassandra/simulator/paxos/Ballots.java index 90cc76a475..c8a1b2b89d 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/paxos/Ballots.java +++ b/test/simulator/main/org/apache/cassandra/simulator/paxos/Ballots.java @@ -117,8 +117,8 @@ public class Ballots long baseTable = latestBallotFromBaseTable(key, metadata); return new LatestBallots( promised.unixMicros(), - accepted == null || accepted.update.isEmpty() ? 0L : accepted.ballot.unixMicros(), - accepted == null || accepted.update.isEmpty() ? 0L : accepted.update.stats().minTimestamp, + accepted == null || accepted.isEmpty() ? 0L : accepted.ballot.unixMicros(), + accepted == null || accepted.isEmpty() ? 0L : accepted.stats().minTimestamp, latestBallot(committed.update.iterator()), baseTable ); diff --git a/test/simulator/main/org/apache/cassandra/simulator/paxos/PairOfSequencesAccordSimulation.java b/test/simulator/main/org/apache/cassandra/simulator/paxos/PairOfSequencesAccordSimulation.java index 391cd654c0..185126cac4 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/paxos/PairOfSequencesAccordSimulation.java +++ b/test/simulator/main/org/apache/cassandra/simulator/paxos/PairOfSequencesAccordSimulation.java @@ -57,6 +57,7 @@ import org.apache.cassandra.simulator.Action; import org.apache.cassandra.simulator.Debug; import org.apache.cassandra.simulator.RunnableActionScheduler; import org.apache.cassandra.simulator.cluster.ClusterActions; +import org.apache.cassandra.simulator.cluster.ReplicationConfig; import org.apache.cassandra.simulator.systems.SimulatedSystems; import org.apache.cassandra.simulator.utils.IntRange; @@ -136,14 +137,16 @@ public class PairOfSequencesAccordSimulation extends AbstractPairOfSequencesPaxo int concurrency, IntRange simulateKeyForSeconds, IntRange withinKeyConcurrency, ConsistencyLevel serialConsistency, RunnableActionScheduler scheduler, Debug debug, long seed, int[] primaryKeys, - long runForNanos, LongSupplier jitter) + long runForNanos, LongSupplier jitter, + ReplicationConfig replicationConfig) { super(simulated, cluster, clusterOptions, readRatio, concurrency, simulateKeyForSeconds, withinKeyConcurrency, serialConsistency, scheduler, debug, seed, primaryKeys, - runForNanos, jitter); + runForNanos, jitter, + replicationConfig); this.transactionalMode = transactionalMode; this.writeRatio = 1F - readRatio; HistoryValidator validator = new StrictSerializabilityValidator(primaryKeys); 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 d4a887c22b..1ca33c4be0 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/paxos/PairOfSequencesPaxosSimulation.java +++ b/test/simulator/main/org/apache/cassandra/simulator/paxos/PairOfSequencesPaxosSimulation.java @@ -43,6 +43,7 @@ import org.apache.cassandra.simulator.ActionListener; import org.apache.cassandra.simulator.Debug; import org.apache.cassandra.simulator.RunnableActionScheduler; import org.apache.cassandra.simulator.cluster.ClusterActions; +import org.apache.cassandra.simulator.cluster.ReplicationConfig; import org.apache.cassandra.simulator.systems.SimulatedSystems; import org.apache.cassandra.simulator.utils.IntRange; import org.apache.cassandra.utils.ByteBufferUtil; @@ -165,14 +166,16 @@ public class PairOfSequencesPaxosSimulation extends AbstractPairOfSequencesPaxos int concurrency, IntRange simulateKeyForSeconds, IntRange withinKeyConcurrency, ConsistencyLevel serialConsistency, RunnableActionScheduler scheduler, Debug debug, long seed, int[] primaryKeys, - long runForNanos, LongSupplier jitter) + long runForNanos, LongSupplier jitter, + ReplicationConfig replicationConfig) { super(simulated, cluster, clusterOptions, readRatio, concurrency, simulateKeyForSeconds, withinKeyConcurrency, serialConsistency, scheduler, debug, seed, primaryKeys, - runForNanos, jitter); + runForNanos, jitter, + replicationConfig); this.transactionalMode = transactionalMode; } 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 cf34b074ca..3ec531335e 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/paxos/PaxosClusterSimulation.java +++ b/test/simulator/main/org/apache/cassandra/simulator/paxos/PaxosClusterSimulation.java @@ -149,7 +149,7 @@ class PaxosClusterSimulation extends ClusterSimulation implemen 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, - () -> jitter.get(random)); + () -> jitter.get(random), builder.replicationConfig()); }); } 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 2d72e4779a..5e19bda1bb 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/paxos/PaxosSimulationRunner.java +++ b/test/simulator/main/org/apache/cassandra/simulator/paxos/PaxosSimulationRunner.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.lang.reflect.Field; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; - import javax.inject.Inject; import org.apache.cassandra.config.Config; @@ -30,7 +29,6 @@ import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.simulator.ClusterSimulation; import org.apache.cassandra.simulator.SimulationRunner; import org.apache.cassandra.simulator.SimulatorUtils; - import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Option; 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 7eb6584a6a..4ec2e3c002 100644 --- a/test/simulator/test/org/apache/cassandra/simulator/test/ShortPaxosSimulationTest.java +++ b/test/simulator/test/org/apache/cassandra/simulator/test/ShortPaxosSimulationTest.java @@ -18,6 +18,7 @@ package org.apache.cassandra.simulator.test; +import org.junit.Ignore; import org.junit.Test; import org.apache.cassandra.simulator.paxos.PaxosSimulationRunner; @@ -106,7 +107,9 @@ public class ShortPaxosSimulationTest "--simulations", String.valueOf(DEFAULT_ITERATIONS) }); } + @Test + @Ignore("fails due to OOM DirectMemory - unclear why") public void selfReconcileTest() { PaxosSimulationRunner.executeWithExceptionThrowing(new String[] { "reconcile", diff --git a/test/simulator/test/org/apache/cassandra/simulator/test/ShortPaxosTrackingSimulationTest.java b/test/simulator/test/org/apache/cassandra/simulator/test/ShortPaxosTrackingSimulationTest.java new file mode 100644 index 0000000000..1799502acc --- /dev/null +++ b/test/simulator/test/org/apache/cassandra/simulator/test/ShortPaxosTrackingSimulationTest.java @@ -0,0 +1,165 @@ +/* + * 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.simulator.test; + +import org.junit.Test; + +import org.apache.cassandra.simulator.paxos.PaxosSimulationRunner; + +/** + * Paxos simulation test with mutation tracking enabled (without witnesses). + * + * Note: Topology changes are disabled (--cluster-actions "") because + * they are not currently supported with mutation tracking. We use + * --cluster-action-limit -1 to ensure all nodes join the ring. + * + * In order to run these tests in your IDE, you need to first build a simulator jar + * + * ant simulator-jars + * + * And then run your test using the following settings (omit add-* if you are running on jdk8): + * + -Dstorage-config=$MODULE_DIR$/test/conf + -Djava.awt.headless=true + -javaagent:$MODULE_DIR$/lib/jamm-0.4.0.jar + -ea + -Dcassandra.debugrefcount=true + -Xss384k + -XX:SoftRefLRUPolicyMSPerMB=0 + -XX:ActiveProcessorCount=2 + -XX:HeapDumpPath=build/test + -Dcassandra.test.driver.connection_timeout_ms=10000 + -Dcassandra.test.driver.read_timeout_ms=24000 + -Dcassandra.memtable_row_overhead_computation_step=100 + -Dcassandra.test.use_prepared=true + -Dcassandra.test.sstableformatdevelopment=true + -Djava.security.egd=file:/dev/urandom + -Dcassandra.testtag=.jdk11 + -Dcassandra.keepBriefBrief=true + -Dcassandra.allow_simplestrategy=true + -Dcassandra.strict.runtime.checks=true + -Dcassandra.reads.thresholds.coordinator.defensive_checks_enabled=true + -Dcassandra.test.flush_local_schema_changes=false + -Dcassandra.test.messagingService.nonGracefulShutdown=true + -Dcassandra.use_nix_recursive_delete=true + -Dcie-cassandra.disable_schema_drop_log=true + -Dlogback.configurationFile=file://$MODULE_DIR$/test/conf/logback-simulator.xml + -Dcassandra.ring_delay_ms=10000 + -Dcassandra.tolerate_sstable_size=true + -Dcassandra.skip_sync=true + -Dcassandra.debugrefcount=false + -Dcassandra.test.simulator.determinismcheck=strict + -Dcassandra.test.simulator.print_asm=none + -javaagent:$MODULE_DIR$/build/test/lib/jars/simulator-asm.jar + -Xbootclasspath/a:$MODULE_DIR$/build/test/lib/jars/simulator-bootstrap.jar + -XX:ActiveProcessorCount=4 + -XX:-TieredCompilation + -XX:-BackgroundCompilation + -XX:CICompilerCount=1 + -XX:Tier4CompileThreshold=1000 + -XX:ReservedCodeCacheSize=256M + -Xmx16G + --add-exports java.base/jdk.internal.misc=ALL-UNNAMED + --add-exports java.base/jdk.internal.ref=ALL-UNNAMED + --add-exports java.base/sun.nio.ch=ALL-UNNAMED + --add-exports java.management.rmi/com.sun.jmx.remote.internal.rmi=ALL-UNNAMED + --add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED + --add-exports java.rmi/sun.rmi.server=ALL-UNNAMED + --add-exports java.sql/java.sql=ALL-UNNAMED + --add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED + --add-opens java.base/java.lang.module=ALL-UNNAMED + --add-opens java.base/java.net=ALL-UNNAMED + --add-opens java.base/jdk.internal.loader=ALL-UNNAMED + --add-opens java.base/jdk.internal.ref=ALL-UNNAMED + --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED + --add-opens java.base/jdk.internal.math=ALL-UNNAMED + --add-opens java.base/jdk.internal.module=ALL-UNNAMED + --add-opens java.base/jdk.internal.util.jar=ALL-UNNAMED + --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED + --add-opens jdk.management.jfr/jdk.management.jfr=ALL-UNNAMED + --add-opens java.desktop/com.sun.beans.introspect=ALL-UNNAMED + */ +public class ShortPaxosTrackingSimulationTest +{ + @Test + public void simulationTestV1() + { + PaxosSimulationRunner.executeWithExceptionThrowing(new String[] { + "run", + "--variant", "v1", + "-n", "3..6", + "-t", "1000", + "-c", "2", + "--cluster-action-limit", "-1", + "--cluster-actions", "", + "--replication-config", "tracking", + "-s", "30" + }); + } + + @Test + public void simulationTestV2() + { + PaxosSimulationRunner.executeWithExceptionThrowing(new String[] { + "run", + "--variant", "v2", + "-n", "3..6", + "-t", "1000", + "-c", "2", + "--cluster-action-limit", "-1", + "--cluster-actions", "", + "--replication-config", "tracking", + "-s", "30" + }); + } + + @Test + public void simulationTestV1NoForwarding() + { + PaxosSimulationRunner.executeWithExceptionThrowing(new String[] { + "run", + "--variant", "v1", + "-n", "3..6", + "-t", "1000", + "-c", "2", + "--cluster-action-limit", "-1", + "--cluster-actions", "", + "--replication-config", "tracking", + "--disable-consensus-forwarding", + "-s", "30" + }); + } + + @Test + public void simulationTestV2NoForwarding() + { + PaxosSimulationRunner.executeWithExceptionThrowing(new String[] { + "run", + "--variant", "v2", + "-n", "3..6", + "-t", "1000", + "-c", "2", + "--cluster-action-limit", "-1", + "--cluster-actions", "", + "--replication-config", "tracking", + "--disable-consensus-forwarding", + "-s", "30" + }); + } +} diff --git a/test/simulator/test/org/apache/cassandra/simulator/test/ShortPaxosWitnessesSimulationTest.java b/test/simulator/test/org/apache/cassandra/simulator/test/ShortPaxosWitnessesSimulationTest.java new file mode 100644 index 0000000000..b542be307e --- /dev/null +++ b/test/simulator/test/org/apache/cassandra/simulator/test/ShortPaxosWitnessesSimulationTest.java @@ -0,0 +1,165 @@ +/* + * 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.simulator.test; + +import org.junit.Test; + +import org.apache.cassandra.simulator.paxos.PaxosSimulationRunner; + +/** + * Paxos simulation test with mutation tracking and witnesses enabled. + * + * Note: Topology changes are disabled (--cluster-actions "") because + * they are not currently supported with witnesses. We use + * --cluster-action-limit -1 to ensure all nodes join the ring. + * + * In order to run these tests in your IDE, you need to first build a simulator jar + * + * ant simulator-jars + * + * And then run your test using the following settings (omit add-* if you are running on jdk8): + * + -Dstorage-config=$MODULE_DIR$/test/conf + -Djava.awt.headless=true + -javaagent:$MODULE_DIR$/lib/jamm-0.4.0.jar + -ea + -Dcassandra.debugrefcount=true + -Xss384k + -XX:SoftRefLRUPolicyMSPerMB=0 + -XX:ActiveProcessorCount=2 + -XX:HeapDumpPath=build/test + -Dcassandra.test.driver.connection_timeout_ms=10000 + -Dcassandra.test.driver.read_timeout_ms=24000 + -Dcassandra.memtable_row_overhead_computation_step=100 + -Dcassandra.test.use_prepared=true + -Dcassandra.test.sstableformatdevelopment=true + -Djava.security.egd=file:/dev/urandom + -Dcassandra.testtag=.jdk11 + -Dcassandra.keepBriefBrief=true + -Dcassandra.allow_simplestrategy=true + -Dcassandra.strict.runtime.checks=true + -Dcassandra.reads.thresholds.coordinator.defensive_checks_enabled=true + -Dcassandra.test.flush_local_schema_changes=false + -Dcassandra.test.messagingService.nonGracefulShutdown=true + -Dcassandra.use_nix_recursive_delete=true + -Dcie-cassandra.disable_schema_drop_log=true + -Dlogback.configurationFile=file://$MODULE_DIR$/test/conf/logback-simulator.xml + -Dcassandra.ring_delay_ms=10000 + -Dcassandra.tolerate_sstable_size=true + -Dcassandra.skip_sync=true + -Dcassandra.debugrefcount=false + -Dcassandra.test.simulator.determinismcheck=strict + -Dcassandra.test.simulator.print_asm=none + -javaagent:$MODULE_DIR$/build/test/lib/jars/simulator-asm.jar + -Xbootclasspath/a:$MODULE_DIR$/build/test/lib/jars/simulator-bootstrap.jar + -XX:ActiveProcessorCount=4 + -XX:-TieredCompilation + -XX:-BackgroundCompilation + -XX:CICompilerCount=1 + -XX:Tier4CompileThreshold=1000 + -XX:ReservedCodeCacheSize=256M + -Xmx16G + --add-exports java.base/jdk.internal.misc=ALL-UNNAMED + --add-exports java.base/jdk.internal.ref=ALL-UNNAMED + --add-exports java.base/sun.nio.ch=ALL-UNNAMED + --add-exports java.management.rmi/com.sun.jmx.remote.internal.rmi=ALL-UNNAMED + --add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED + --add-exports java.rmi/sun.rmi.server=ALL-UNNAMED + --add-exports java.sql/java.sql=ALL-UNNAMED + --add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED + --add-opens java.base/java.lang.module=ALL-UNNAMED + --add-opens java.base/java.net=ALL-UNNAMED + --add-opens java.base/jdk.internal.loader=ALL-UNNAMED + --add-opens java.base/jdk.internal.ref=ALL-UNNAMED + --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED + --add-opens java.base/jdk.internal.math=ALL-UNNAMED + --add-opens java.base/jdk.internal.module=ALL-UNNAMED + --add-opens java.base/jdk.internal.util.jar=ALL-UNNAMED + --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED + --add-opens jdk.management.jfr/jdk.management.jfr=ALL-UNNAMED + --add-opens java.desktop/com.sun.beans.introspect=ALL-UNNAMED + */ +public class ShortPaxosWitnessesSimulationTest +{ + @Test + public void simulationTestV1() + { + PaxosSimulationRunner.executeWithExceptionThrowing(new String[] { + "run", + "--variant", "v1", + "-n", "3..6", + "-t", "1000", + "-c", "2", + "--cluster-action-limit", "-1", + "--cluster-actions", "", + "--replication-config", "witnesses", + "-s", "30" + }); + } + + @Test + public void simulationTestV2() + { + PaxosSimulationRunner.executeWithExceptionThrowing(new String[] { + "run", + "--variant", "v2", + "-n", "3..6", + "-t", "1000", + "-c", "2", + "--cluster-action-limit", "-1", + "--cluster-actions", "", + "--replication-config", "witnesses", + "-s", "30" + }); + } + + @Test + public void simulationTestV1NoForwarding() + { + PaxosSimulationRunner.executeWithExceptionThrowing(new String[] { + "run", + "--variant", "v1", + "-n", "3..6", + "-t", "1000", + "-c", "2", + "--cluster-action-limit", "-1", + "--cluster-actions", "", + "--replication-config", "witnesses", + "--disable-consensus-forwarding", + "-s", "30" + }); + } + + @Test + public void simulationTestV2NoForwarding() + { + PaxosSimulationRunner.executeWithExceptionThrowing(new String[] { + "run", + "--variant", "v2", + "-n", "3..6", + "-t", "1000", + "-c", "2", + "--cluster-action-limit", "-1", + "--cluster-actions", "", + "--replication-config", "witnesses", + "--disable-consensus-forwarding", + "-s", "30" + }); + } +} diff --git a/test/unit/org/apache/cassandra/cql3/statements/CQL3CasRequestSerializationTest.java b/test/unit/org/apache/cassandra/cql3/statements/CQL3CasRequestSerializationTest.java new file mode 100644 index 0000000000..2e29c50f42 --- /dev/null +++ b/test/unit/org/apache/cassandra/cql3/statements/CQL3CasRequestSerializationTest.java @@ -0,0 +1,523 @@ +/* + * 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.cql3.statements; + +import java.io.IOException; + +import org.junit.Before; +import org.junit.Test; + +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.cql3.QueryOptions; +import org.apache.cassandra.cql3.QueryProcessor; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.RegularAndStaticColumns; +import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.io.util.DataInputBuffer; +import org.apache.cassandra.io.util.DataOutputBuffer; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.utils.FBUtilities; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class CQL3CasRequestSerializationTest extends CQLTester +{ + private static final int DEFAULT_PK_VALUE = 1; + private static final int DEFAULT_CK_VALUE = 1; + + private TableMetadata metadata; + private DecoratedKey partitionKey; + private RegularAndStaticColumns conditionColumns; + private ClientState clientState; + + @Before + public void setUp() throws Throwable + { + createTable("CREATE TABLE %s (pk int, ck int, v1 int, v2 text, s int static, PRIMARY KEY (pk, ck))"); + metadata = currentTableMetadata(); + partitionKey = createDecoratedKey(metadata, DEFAULT_PK_VALUE); + conditionColumns = metadata.regularAndStaticColumns(); + clientState = ClientState.forInternalCalls(); + } + + // === Helper Methods === + + /** + * Creates a DecoratedKey for the given table metadata and partition key value. + */ + private static DecoratedKey createDecoratedKey(TableMetadata metadata, int pkValue) + { + return metadata.partitioner.decorateKey(Int32Type.instance.decompose(pkValue)); + } + + /** + * Creates a Clustering for the given clustering key value. + */ + private static Clustering createClustering(int ckValue) + { + return Clustering.make(Int32Type.instance.decompose(ckValue)); + } + + /** + * Sets up a new table and returns a TestTableSetup with all necessary components. + */ + private TestTableSetup setupTable(String createTableSql) + { + createTable(createTableSql); + TableMetadata tableMetadata = currentTableMetadata(); + assertSchemaRegistration(tableMetadata); + + return new TestTableSetup( + tableMetadata, + createDecoratedKey(tableMetadata, DEFAULT_PK_VALUE), + tableMetadata.regularAndStaticColumns() + ); + } + + /** + * Verifies that the table metadata is properly registered in the global schema. + * This is critical for TxnWrite.Fragment serialization which relies on Schema.instance lookups. + */ + private static void assertSchemaRegistration(TableMetadata metadata) + { + TableMetadata schemaMetadata = Schema.instance.getTableMetadata(metadata.id); + assertNotNull("Table metadata must be available in Schema.instance for serialization", schemaMetadata); + assertEquals("Schema metadata must match our test metadata", metadata.id, schemaMetadata.id); + assertEquals("Schema metadata keyspace must match", metadata.keyspace, schemaMetadata.keyspace); + assertEquals("Schema metadata name must match", metadata.name, schemaMetadata.name); + } + + /** + * Creates a CQL statement from SQL and returns the parsed ModificationStatement. + */ + private ModificationStatement createStatement(String sql, String keyspace, String tableName) + { + String formattedSql = String.format(sql, keyspace, tableName); + return (ModificationStatement) QueryProcessor.parseStatement(formattedSql, clientState); + } + + /** + * Performs round-trip serialization test with assertion. + */ + private static void assertRoundTripSerialization(CQL3CasRequest original, String testDescription) throws IOException + { + CQL3CasRequest deserialized = serdes(original); + assertNotNull(deserialized); + assertEquals("Round-trip serialization should preserve " + testDescription, original, deserialized); + } + + /** + * Helper class to hold table setup components. + */ + private static class TestTableSetup + { + final TableMetadata metadata; + final DecoratedKey key; + final RegularAndStaticColumns conditionColumns; + + TestTableSetup(TableMetadata metadata, DecoratedKey key, RegularAndStaticColumns conditionColumns) + { + this.metadata = metadata; + this.key = key; + this.conditionColumns = conditionColumns; + } + + CQL3CasRequest createRequest(boolean updatesRegular, boolean updatesStatic) + { + return new CQL3CasRequest(metadata, key, conditionColumns, updatesRegular, updatesStatic); + } + } + + /** + * Performs round-trip serialization/deserialization with size validation. + */ + private static CQL3CasRequest serdes(CQL3CasRequest request) throws IOException + { + CQL3CasRequest.Serializer serializer = CQL3CasRequest.serializer; + int expectedSize = (int) serializer.serializedSize(request, MessagingService.current_version); + + try (DataOutputBuffer out = new DataOutputBuffer(expectedSize)) + { + serializer.serialize(request, out, MessagingService.current_version); + assertEquals("Serialized size must match calculated size", expectedSize, out.buffer().limit()); + + try (DataInputBuffer in = new DataInputBuffer(out.buffer(), false)) + { + return serializer.deserialize(in, MessagingService.current_version); + } + } + } + + @Test + public void testBasicRoundTrip() throws Throwable + { + CQL3CasRequest original = new CQL3CasRequest(metadata, partitionKey, conditionColumns, true, false); + + CQL3CasRequest deserialized = serdes(original); + + assertNotNull(deserialized); + assertEquals("Round-trip serialization should preserve all fields", original, deserialized); + } + + @Test + public void testNotExistsCondition() throws Throwable + { + CQL3CasRequest original = new CQL3CasRequest(metadata, partitionKey, conditionColumns, true, false); + Clustering clustering = Clustering.make(Int32Type.instance.decompose(1)); + original.addNotExist(clustering); + + CQL3CasRequest deserialized = serdes(original); + + assertNotNull(deserialized); + assertEquals("Round-trip serialization should preserve NotExists condition", original, deserialized); + } + + @Test + public void testExistsCondition() throws Throwable + { + CQL3CasRequest original = new CQL3CasRequest(metadata, partitionKey, conditionColumns, true, false); + Clustering clustering = Clustering.make(Int32Type.instance.decompose(1)); + original.addExist(clustering); + + CQL3CasRequest deserialized = serdes(original); + + assertNotNull(deserialized); + assertEquals("Round-trip serialization should preserve Exists condition", original, deserialized); + } + + @Test + public void testStaticRowConditions() throws Throwable + { + CQL3CasRequest original = new CQL3CasRequest(metadata, partitionKey, conditionColumns, false, true); + original.addNotExist(Clustering.STATIC_CLUSTERING); + + CQL3CasRequest deserialized = serdes(original); + + assertNotNull(deserialized); + assertEquals("Round-trip serialization should preserve static row conditions", original, deserialized); + } + + @Test + public void testColumnConditions() throws Throwable + { + // This test verifies serialization works for requests that could have column conditions + // The actual column conditions are added through the CQL processing layer in practice + CQL3CasRequest original = new CQL3CasRequest(metadata, partitionKey, conditionColumns, true, false); + Clustering clustering = Clustering.make(Int32Type.instance.decompose(1)); + + // Add a basic existence condition for serialization testing + original.addExist(clustering); + + CQL3CasRequest deserialized = serdes(original); + + assertNotNull(deserialized); + assertEquals("Round-trip serialization should preserve column conditions", original, deserialized); + } + + @Test + public void testMixedConditionsAndUpdates() throws Throwable + { + CQL3CasRequest original = new CQL3CasRequest(metadata, partitionKey, conditionColumns, true, true); + + // Add both static and regular conditions + original.addNotExist(Clustering.STATIC_CLUSTERING); + Clustering clustering = Clustering.make(Int32Type.instance.decompose(1)); + original.addExist(clustering); + + CQL3CasRequest deserialized = serdes(original); + + assertNotNull(deserialized); + assertEquals("Round-trip serialization should preserve mixed conditions", original, deserialized); + } + + @Test + public void testWithWriteFragments() throws Throwable + { + // Verify schema is properly registered before testing fragments + assertSchemaRegistration(metadata); + + CQL3CasRequest original = new CQL3CasRequest(metadata, partitionKey, conditionColumns, true, false); + + // Create a simple INSERT statement to generate a writeFragment + String insertCql = String.format("INSERT INTO %s.%s (pk, ck, v1) VALUES (1, 1, 100)", + metadata.keyspace, metadata.name); + ModificationStatement stmt = (ModificationStatement) QueryProcessor.parseStatement(insertCql, clientState); + + // Add write fragment to the request + QueryOptions options = QueryOptions.DEFAULT; + original.addWriteFragment(stmt, options, clientState, FBUtilities.nowInSeconds()); + + CQL3CasRequest deserialized = serdes(original); + + assertNotNull(deserialized); + assertEquals("Round-trip serialization should preserve request with writeFragments", original, deserialized); + } + + @Test + public void testMultipleWriteFragments() throws Throwable + { + // Verify schema is properly registered before testing fragments + assertSchemaRegistration(metadata); + + CQL3CasRequest original = new CQL3CasRequest(metadata, partitionKey, conditionColumns, true, false); + + // Create multiple statements to generate multiple writeFragments + String insertCql1 = String.format("INSERT INTO %s.%s (pk, ck, v1) VALUES (1, 1, 100)", + metadata.keyspace, metadata.name); + String insertCql2 = String.format("INSERT INTO %s.%s (pk, ck, v2) VALUES (1, 2, 'test')", + metadata.keyspace, metadata.name); + + ModificationStatement stmt1 = (ModificationStatement) QueryProcessor.parseStatement(insertCql1, clientState); + + ModificationStatement stmt2 = (ModificationStatement) QueryProcessor.parseStatement(insertCql2, clientState); + + // Add multiple write fragments to the request + QueryOptions options = QueryOptions.DEFAULT; + original.addWriteFragment(stmt1, options, clientState, FBUtilities.nowInSeconds()); + original.addWriteFragment(stmt2, options, clientState, FBUtilities.nowInSeconds()); + + CQL3CasRequest deserialized = serdes(original); + + assertNotNull(deserialized); + assertEquals("Round-trip serialization should preserve request with multiple writeFragments", original, deserialized); + } + + @Test + public void testNullStaticConditions() throws Throwable + { + // Test with no static conditions (null case) + CQL3CasRequest original = new CQL3CasRequest(metadata, partitionKey, conditionColumns, true, false); + Clustering clustering = Clustering.make(Int32Type.instance.decompose(1)); + original.addExist(clustering); // Only regular conditions, no static + + CQL3CasRequest deserialized = serdes(original); + + assertNotNull(deserialized); + assertEquals("Round-trip serialization should preserve null static conditions", original, deserialized); + } + + @Test + public void testEmptyConditionsMap() throws Throwable + { + // Test with only static conditions (empty regular conditions map) + CQL3CasRequest original = new CQL3CasRequest(metadata, partitionKey, conditionColumns, false, true); + original.addExist(Clustering.STATIC_CLUSTERING); // Only static condition + + CQL3CasRequest deserialized = serdes(original); + + assertNotNull(deserialized); + assertEquals("Round-trip serialization should preserve empty conditions map", original, deserialized); + } + + @Test + public void testComplexScenario() throws Throwable + { + // Verify schema is properly registered before testing fragments + assertSchemaRegistration(metadata); + + CQL3CasRequest original = new CQL3CasRequest(metadata, partitionKey, conditionColumns, true, true); + + // Mix of conditions + original.addNotExist(Clustering.STATIC_CLUSTERING); + + Clustering clustering1 = Clustering.make(Int32Type.instance.decompose(1)); + original.addExist(clustering1); + + Clustering clustering2 = Clustering.make(Int32Type.instance.decompose(2)); + original.addNotExist(clustering2); + + // Add writeFragments as part of complex scenario + String insertCql = String.format("INSERT INTO %s.%s (pk, ck, v1, s) VALUES (1, 3, 300, 500)", + metadata.keyspace, metadata.name); + ModificationStatement stmt = (ModificationStatement) QueryProcessor.parseStatement(insertCql, clientState); + + QueryOptions options = QueryOptions.DEFAULT; + original.addWriteFragment(stmt, options, clientState, FBUtilities.nowInSeconds()); + + CQL3CasRequest deserialized = serdes(original); + + assertNotNull(deserialized); + assertEquals("Round-trip serialization should preserve complex conditions and writeFragments scenario", original, deserialized); + } + + @Test + public void testListAppendReferenceOperations() throws Throwable + { + // Create a table with list columns to test list append operations + String tableName = createTable("CREATE TABLE %s (pk int, ck int, list_col list, static_list_col list static, PRIMARY KEY (pk, ck))"); + TableMetadata listMetadata = currentTableMetadata(); + + // Verify schema is properly registered before testing fragments + TableMetadata schemaMetadata = Schema.instance.getTableMetadata(listMetadata.id); + assertNotNull("Table metadata must be available in Schema.instance for serialization", schemaMetadata); + assertEquals("Schema metadata must match our test metadata", listMetadata.id, schemaMetadata.id); + + DecoratedKey key = listMetadata.partitioner.decorateKey(Int32Type.instance.decompose(1)); + RegularAndStaticColumns condColumns = listMetadata.regularAndStaticColumns(); + + CQL3CasRequest original = new CQL3CasRequest(listMetadata, key, condColumns, true, true); + + // Create list append statement - this creates reference operations + String listAppendCql = String.format("UPDATE %s.%s SET list_col = list_col + [1, 2] WHERE pk = 1 AND ck = 1 IF EXISTS", + listMetadata.keyspace, listMetadata.name); + ModificationStatement stmt = (ModificationStatement) QueryProcessor.parseStatement(listAppendCql, clientState); + + QueryOptions options = QueryOptions.DEFAULT; + + // Add conditions for the existence check + Clustering clustering = Clustering.make(Int32Type.instance.decompose(1)); + original.addExist(clustering); + + // Add write fragment - this should create non-empty TxnReferenceOperations + original.addWriteFragment(stmt, options, clientState, FBUtilities.nowInSeconds()); + + CQL3CasRequest deserialized = serdes(original); + + assertNotNull(deserialized); + assertEquals("Round-trip serialization should preserve list append reference operations", original, deserialized); + } + + @Test + public void testStaticListAppendReferenceOperations() throws Throwable + { + // Create a table with static list columns to test static reference operations + String tableName = createTable("CREATE TABLE %s (pk int, ck int, v int, static_list_col list static, PRIMARY KEY (pk, ck))"); + TableMetadata listMetadata = currentTableMetadata(); + + // Verify schema is properly registered before testing fragments + TableMetadata schemaMetadata = Schema.instance.getTableMetadata(listMetadata.id); + assertNotNull("Table metadata must be available in Schema.instance for serialization", schemaMetadata); + assertEquals("Schema metadata must match our test metadata", listMetadata.id, schemaMetadata.id); + + DecoratedKey key = listMetadata.partitioner.decorateKey(Int32Type.instance.decompose(1)); + RegularAndStaticColumns condColumns = listMetadata.regularAndStaticColumns(); + + CQL3CasRequest original = new CQL3CasRequest(listMetadata, key, condColumns, true, true); + + // Create static list append statement - this creates static reference operations + String staticListAppendCql = String.format("UPDATE %s.%s SET static_list_col = static_list_col + ['test', 'value'] WHERE pk = 1 IF EXISTS", + listMetadata.keyspace, listMetadata.name); + ModificationStatement stmt = (ModificationStatement) QueryProcessor.parseStatement(staticListAppendCql, clientState); + + QueryOptions options = QueryOptions.DEFAULT; + + // Add existence condition (even though we're updating static columns, the test framework expects conditions) + // This doesn't need to semantically match the CQL - it's about testing serialization + Clustering clustering = Clustering.make(Int32Type.instance.decompose(1)); + original.addExist(clustering); + + // Add write fragment - this should create non-empty TxnReferenceOperations.statics + original.addWriteFragment(stmt, options, clientState, FBUtilities.nowInSeconds()); + + CQL3CasRequest deserialized = serdes(original); + + assertNotNull(deserialized); + assertEquals("Round-trip serialization should preserve static list append reference operations", original, deserialized); + } + + @Test + public void testNumericIncrementReferenceOperations() throws Throwable + { + // Create a table with regular numeric columns to test increment operations + String tableName = createTable("CREATE TABLE %s (pk int, ck int, numeric_col int, static_numeric int static, PRIMARY KEY (pk, ck))"); + TableMetadata numericMetadata = currentTableMetadata(); + + // Verify schema is properly registered before testing fragments + TableMetadata schemaMetadata = Schema.instance.getTableMetadata(numericMetadata.id); + assertNotNull("Table metadata must be available in Schema.instance for serialization", schemaMetadata); + assertEquals("Schema metadata must match our test metadata", numericMetadata.id, schemaMetadata.id); + + DecoratedKey key = numericMetadata.partitioner.decorateKey(Int32Type.instance.decompose(1)); + RegularAndStaticColumns condColumns = numericMetadata.regularAndStaticColumns(); + + CQL3CasRequest original = new CQL3CasRequest(numericMetadata, key, condColumns, true, true); + + // Create numeric increment statement - this creates reference operations + String numericIncrementCql = String.format("UPDATE %s.%s SET numeric_col = numeric_col + 5 WHERE pk = 1 AND ck = 1 IF EXISTS", + numericMetadata.keyspace, numericMetadata.name); + ModificationStatement stmt = (ModificationStatement) QueryProcessor.parseStatement(numericIncrementCql, clientState); + + QueryOptions options = QueryOptions.DEFAULT; + + // Add conditions for the existence check + Clustering clustering = Clustering.make(Int32Type.instance.decompose(1)); + original.addExist(clustering); + + // Add write fragment - this should create non-empty TxnReferenceOperations with numeric increment operations + original.addWriteFragment(stmt, options, clientState, FBUtilities.nowInSeconds()); + + CQL3CasRequest deserialized = serdes(original); + + assertNotNull(deserialized); + assertEquals("Round-trip serialization should preserve numeric increment reference operations", original, deserialized); + } + + @Test + public void testMixedReferenceOperations() throws Throwable + { + // Create a table with both list and map columns + String tableName = createTable("CREATE TABLE %s (pk int, ck int, list_col list, map_col map, static_list_col list static, static_map_col map static, PRIMARY KEY (pk, ck))"); + TableMetadata mixedMetadata = currentTableMetadata(); + + // Verify schema is properly registered before testing fragments + TableMetadata schemaMetadata = Schema.instance.getTableMetadata(mixedMetadata.id); + assertNotNull("Table metadata must be available in Schema.instance for serialization", schemaMetadata); + assertEquals("Schema metadata must match our test metadata", mixedMetadata.id, schemaMetadata.id); + + DecoratedKey key = mixedMetadata.partitioner.decorateKey(Int32Type.instance.decompose(1)); + RegularAndStaticColumns condColumns = mixedMetadata.regularAndStaticColumns(); + + CQL3CasRequest original = new CQL3CasRequest(mixedMetadata, key, condColumns, true, true); + + // Add conditions for existence check + Clustering clustering = Clustering.make(Int32Type.instance.decompose(1)); + original.addExist(clustering); + + // Create list append statement + String listAppendCql = String.format("UPDATE %s.%s SET list_col = list_col + [10, 20] WHERE pk = 1 AND ck = 1 IF EXISTS", + mixedMetadata.keyspace, mixedMetadata.name); + ModificationStatement listStmt = (ModificationStatement) QueryProcessor.parseStatement(listAppendCql, clientState); + + // Create map update statement + String mapUpdateCql = String.format("UPDATE %s.%s SET map_col = map_col + {'key1': 100} WHERE pk = 1 AND ck = 1 IF EXISTS", + mixedMetadata.keyspace, mixedMetadata.name); + ModificationStatement mapStmt = (ModificationStatement) QueryProcessor.parseStatement(mapUpdateCql, clientState); + + // Create static list append statement + String staticListAppendCql = String.format("UPDATE %s.%s SET static_list_col = static_list_col + ['static', 'test'] WHERE pk = 1 IF EXISTS", + mixedMetadata.keyspace, mixedMetadata.name); + ModificationStatement staticListStmt = (ModificationStatement) QueryProcessor.parseStatement(staticListAppendCql, clientState); + + QueryOptions options = QueryOptions.DEFAULT; + + // Add write fragments - this should create TxnReferenceOperations with both regular and static operations + original.addWriteFragment(listStmt, options, clientState, FBUtilities.nowInSeconds()); + original.addWriteFragment(mapStmt, options, clientState, FBUtilities.nowInSeconds()); + original.addWriteFragment(staticListStmt, options, clientState, FBUtilities.nowInSeconds()); + + CQL3CasRequest deserialized = serdes(original); + + assertNotNull(deserialized); + assertEquals("Round-trip serialization should preserve mixed reference operations (list, map, static)", original, deserialized); + } +} \ No newline at end of file diff --git a/test/unit/org/apache/cassandra/db/CleanupTransientTest.java b/test/unit/org/apache/cassandra/db/CleanupTransientTest.java index bf4f65f253..6c83418f91 100644 --- a/test/unit/org/apache/cassandra/db/CleanupTransientTest.java +++ b/test/unit/org/apache/cassandra/db/CleanupTransientTest.java @@ -41,6 +41,7 @@ import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.RangesAtEndpoint; import org.apache.cassandra.locator.Replica; +import org.apache.cassandra.replication.MutationJournal; import org.apache.cassandra.replication.MutationTrackingService; import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.service.StorageService; @@ -76,11 +77,6 @@ public class CleanupTransientTest extends CassandraTestBase { DatabaseDescriptor.setMutationTrackingEnabled(true); DatabaseDescriptor.setTransientReplicationEnabledUnsafe(true); - SchemaLoader.createKeyspace(KEYSPACE1, - KeyspaceParams.simpleWitness("2/1"), - SchemaLoader.standardCFMD(KEYSPACE1, CF_STANDARD1), - SchemaLoader.compositeIndexCFMD(KEYSPACE1, CF_INDEXED1, true)); - StorageService ss = StorageService.instance; final int RING_SIZE = 2; @@ -93,6 +89,11 @@ public class CleanupTransientTest extends CassandraTestBase endpointTokens.add(RandomPartitioner.instance.midpoint(RandomPartitioner.MINIMUM, new RandomPartitioner.BigIntegerToken(RandomPartitioner.MAXIMUM))); Util.createInitialRing(endpointTokens, keyTokens, hosts, hostIds, RING_SIZE); + MutationJournal.instance.start(); + SchemaLoader.createKeyspace(KEYSPACE1, + KeyspaceParams.simpleWitness("2/1"), + SchemaLoader.standardCFMD(KEYSPACE1, CF_STANDARD1), + SchemaLoader.compositeIndexCFMD(KEYSPACE1, CF_INDEXED1, true)); } @Test diff --git a/test/unit/org/apache/cassandra/db/IReadResponseSerializerTest.java b/test/unit/org/apache/cassandra/db/IReadResponseSerializerTest.java new file mode 100644 index 0000000000..14e3fb7fa4 --- /dev/null +++ b/test/unit/org/apache/cassandra/db/IReadResponseSerializerTest.java @@ -0,0 +1,358 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.db; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.config.DatabaseDescriptor; +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.marshal.Int32Type; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; +import org.apache.cassandra.io.util.DataInputBuffer; +import org.apache.cassandra.io.util.DataOutputBuffer; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.replication.MutationSummary; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.reads.tracked.TrackedDataResponse; +import org.apache.cassandra.service.reads.tracked.TrackedRead; +import org.apache.cassandra.service.reads.tracked.TrackedSummaryResponse; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.FBUtilities; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +/** + * Comprehensive tests for IReadResponse serializer covering all response kinds + * (UNTRACKED, TRACKED_DATA, TRACKED_SUMMARY, NULL) and all supported versions. + */ +public class IReadResponseSerializerTest +{ + private TableMetadata metadata; + + @BeforeClass + public static void beforeClass() + { + DatabaseDescriptor.daemonInitialization(); + ClusterMetadataTestHelper.setInstanceForTest(); + // Register the local node so that ClusterMetadata.current().myNodeId() returns a valid NodeId. + // This is needed for TrackedRead.Id's static initializer. + ClusterMetadataTestHelper.register(FBUtilities.getBroadcastAddressAndPort()); + } + + @Before + public void setup() + { + metadata = TableMetadata.builder("ks", "t1") + .offline() + .addPartitionKeyColumn("p", Int32Type.instance) + .addRegularColumn("v", Int32Type.instance) + .partitioner(Murmur3Partitioner.instance) + .build(); + } + + // ==================== UNTRACKED (ReadResponse) Tests ==================== + + @Test + public void testUntrackedResponseAllVersions() throws IOException + { + ReadResponse response = createReadResponse(); + + for (MessagingService.Version version : MessagingService.Version.supportedVersions()) + { + testRoundTrip(response, version.value, ReadKind.UNTRACKED); + } + } + + @Test + public void testUntrackedResponseSerializedSizeMatchesActual() throws IOException + { + ReadResponse response = createReadResponse(); + + for (MessagingService.Version version : MessagingService.Version.supportedVersions()) + { + DataOutputBuffer out = new DataOutputBuffer(); + IReadResponse.serializer.serialize(response, out, version.value); + + long expectedSize = IReadResponse.serializer.serializedSize(response, version.value); + assertEquals("Serialized size should match actual bytes written for version " + version, + expectedSize, out.buffer().remaining()); + } + } + + // ==================== TRACKED_DATA Tests ==================== + + @Test + public void testTrackedDataResponseVersion52Plus() throws IOException + { + TrackedDataResponse response = createTrackedDataResponse(); + + for (MessagingService.Version version : MessagingService.Version.supportedVersions()) + { + if (version.value >= MessagingService.VERSION_61) + { + testRoundTrip(response, version.value, ReadKind.TRACKED_DATA); + } + } + } + + @Test + public void testTrackedDataResponseSerializedSizeMatchesActual() throws IOException + { + TrackedDataResponse response = createTrackedDataResponse(); + + for (MessagingService.Version version : MessagingService.Version.supportedVersions()) + { + if (version.value >= MessagingService.VERSION_61) + { + DataOutputBuffer out = new DataOutputBuffer(); + IReadResponse.serializer.serialize(response, out, version.value); + + long expectedSize = IReadResponse.serializer.serializedSize(response, version.value); + assertEquals("Serialized size should match actual bytes written for version " + version, + expectedSize, out.buffer().remaining()); + } + } + } + + @Test + public void testTrackedDataResponsePreVersion52Rejected() + { + TrackedDataResponse response = createTrackedDataResponse(); + + for (MessagingService.Version version : MessagingService.Version.supportedVersions()) + { + if (version.value < MessagingService.VERSION_61) + { + try + { + DataOutputBuffer out = new DataOutputBuffer(); + IReadResponse.serializer.serialize(response, out, version.value); + fail("Should have thrown for TRACKED_DATA on pre-VERSION_61: " + version); + } + catch (IllegalArgumentException | IOException e) + { + // Expected - pre-VERSION_61 should not support tracked responses + } + } + } + } + + // ==================== TRACKED_SUMMARY Tests ==================== + + @Test + public void testTrackedSummaryResponseVersion52Plus() throws IOException + { + TrackedSummaryResponse response = createTrackedSummaryResponse(); + + for (MessagingService.Version version : MessagingService.Version.supportedVersions()) + { + if (version.value >= MessagingService.VERSION_61) + { + testRoundTrip(response, version.value, ReadKind.TRACKED_SUMMARY); + } + } + } + + @Test + public void testTrackedSummaryResponseSerializedSizeMatchesActual() throws IOException + { + TrackedSummaryResponse response = createTrackedSummaryResponse(); + + for (MessagingService.Version version : MessagingService.Version.supportedVersions()) + { + if (version.value >= MessagingService.VERSION_61) + { + DataOutputBuffer out = new DataOutputBuffer(); + IReadResponse.serializer.serialize(response, out, version.value); + + long expectedSize = IReadResponse.serializer.serializedSize(response, version.value); + assertEquals("Serialized size should match actual bytes written for version " + version, + expectedSize, out.buffer().remaining()); + } + } + } + + @Test + public void testTrackedSummaryResponsePreVersion52Rejected() + { + TrackedSummaryResponse response = createTrackedSummaryResponse(); + + for (MessagingService.Version version : MessagingService.Version.supportedVersions()) + { + if (version.value < MessagingService.VERSION_61) + { + try + { + DataOutputBuffer out = new DataOutputBuffer(); + IReadResponse.serializer.serialize(response, out, version.value); + fail("Should have thrown for TRACKED_SUMMARY on pre-VERSION_61: " + version); + } + catch (IllegalArgumentException | IOException e) + { + // Expected - pre-VERSION_61 should not support tracked responses + } + } + } + } + + // ==================== Kind Serializer Tests ==================== + + @Test + public void testKindSerializerRoundTrip() throws IOException + { + for (ReadKind kind : ReadKind.values()) + { + for (MessagingService.Version version : MessagingService.Version.supportedVersions()) + { + DataOutputBuffer out = new DataOutputBuffer(); + ReadKind.serializer.serialize(kind, out); + + long expectedSize = ReadKind.serializer.serializedSize(kind); + assertEquals("Kind serializedSize should match actual bytes for " + kind + " version " + version, + expectedSize, out.buffer().remaining()); + + DataInputBuffer in = new DataInputBuffer(out.buffer(), false); + ReadKind deserialized = ReadKind.serializer.deserialize(in); + + assertEquals("Kind should round-trip correctly for " + kind + " version " + version, + kind, deserialized); + } + } + } + + @Test + public void testKindIsTracked() + { + assertEquals("UNTRACKED.isTracked() should be false", false, ReadKind.UNTRACKED.isTracked()); + assertEquals("TRACKED_DATA.isTracked() should be true", true, ReadKind.TRACKED_DATA.isTracked()); + assertEquals("TRACKED_SUMMARY.isTracked() should be true", true, ReadKind.TRACKED_SUMMARY.isTracked()); + } + + // ==================== Helper Methods ==================== + + private void testRoundTrip(IReadResponse response, int version, ReadKind expectedKind) throws IOException + { + DataOutputBuffer out = new DataOutputBuffer(); + IReadResponse.serializer.serialize(response, out, version); + + DataInputBuffer in = new DataInputBuffer(out.buffer(), false); + IReadResponse deserialized = IReadResponse.serializer.deserialize(in, version); + + assertNotNull("Deserialized response should not be null for version " + version, deserialized); + assertEquals("Response kind should match for version " + version, expectedKind, deserialized.kind()); + } + + private ReadResponse createReadResponse() + { + ReadCommand command = createReadCommand(); + return command.createResponse(EmptyIterators.unfilteredPartition(metadata), + new StubRepairedDataInfo(ByteBufferUtil.EMPTY_BYTE_BUFFER, true)); + } + + private TrackedDataResponse createTrackedDataResponse() + { + // Create a simple TrackedDataResponse with test data + ByteBuffer testData = ByteBuffer.wrap(new byte[] { 1, 2, 3, 4, 5 }); + return new TrackedDataResponse(MessagingService.current_version, testData); + } + + private TrackedSummaryResponse createTrackedSummaryResponse() + { + // Create a TrackedRead.Id with test values using the public constructor + TrackedRead.Id readId = new TrackedRead.Id(1, 12345L); + // Create an empty MutationSummary for testing + MutationSummary summary = new MutationSummary.Builder(metadata.id).build(); + int dataNode = 1; + int[] summaryNodes = new int[] { 2, 3 }; + return new TrackedSummaryResponse(readId, summary, dataNode, summaryNodes); + } + + private ReadCommand createReadCommand() + { + return new StubReadCommand(1, metadata, false); + } + + private static class StubRepairedDataInfo extends RepairedDataInfo + { + private final ByteBuffer repairedDigest; + private final boolean conclusive; + + public StubRepairedDataInfo(ByteBuffer repairedDigest, boolean conclusive) + { + super(null); + this.repairedDigest = repairedDigest; + this.conclusive = conclusive; + } + + @Override + public ByteBuffer getDigest() + { + return repairedDigest; + } + + @Override + public boolean isConclusive() + { + return conclusive; + } + } + + private static class StubReadCommand extends SinglePartitionReadCommand + { + StubReadCommand(int key, TableMetadata metadata, boolean isDigest) + { + super(metadata.epoch, + isDigest, + 0, + PotentialTxnConflicts.DISALLOW, + metadata, + FBUtilities.nowInSeconds(), + ColumnFilter.all(metadata), + RowFilter.none(), + DataLimits.NONE, + metadata.partitioner.decorateKey(ByteBufferUtil.bytes(key)), + null, + null, + false, + null); + } + + @Override + public boolean selectsFullPartition() + { + return true; + } + + public UnfilteredPartitionIterator executeLocally(ReadExecutionController controller) + { + return EmptyIterators.unfilteredPartition(this.metadata()); + } + } +} diff --git a/test/unit/org/apache/cassandra/db/guardrails/ColumnTypeSpecificValueThresholdTester.java b/test/unit/org/apache/cassandra/db/guardrails/ColumnTypeSpecificValueThresholdTester.java index 56de5bcbbe..75d04b89b9 100644 --- a/test/unit/org/apache/cassandra/db/guardrails/ColumnTypeSpecificValueThresholdTester.java +++ b/test/unit/org/apache/cassandra/db/guardrails/ColumnTypeSpecificValueThresholdTester.java @@ -189,14 +189,14 @@ public abstract class ColumnTypeSpecificValueThresholdTester extends ValueThresh // static column assertValid("INSERT INTO %s (k, s) VALUES ('1', ?) IF NOT EXISTS", allocate(1)); assertValid("INSERT INTO %s (k, s) VALUES ('2', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD)); - assertValid("INSERT INTO %s (k, s) VALUES ('2', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); // not applied - assertWarns("s", "INSERT INTO %s (k, s) VALUES ('3', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); + assertWarns("s", "INSERT INTO %s (k, s) VALUES ('2', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); // not applied + assertWarns("s", "INSERT INTO %s (k, s) VALUES ('3', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); // applied // regular column assertValid("INSERT INTO %s (k, c, v) VALUES ('4', '0', ?) IF NOT EXISTS", allocate(1)); assertValid("INSERT INTO %s (k, c, v) VALUES ('5', '0', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD)); - assertValid("INSERT INTO %s (k, c, v) VALUES ('5', '0', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); // not applied - assertWarns("v", "INSERT INTO %s (k, c, v) VALUES ('6', '0', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); + assertWarns("v", "INSERT INTO %s (k, c, v) VALUES ('5', '0', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); // not applied + assertWarns("v", "INSERT INTO %s (k, c, v) VALUES ('6', '0', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); // applied } @Test @@ -210,17 +210,17 @@ public abstract class ColumnTypeSpecificValueThresholdTester extends ValueThresh // clustering key, the CAS updates with values beyond the threshold are not applied, so they don't come to fail testNoThreshold("UPDATE %s SET v = '0' WHERE k = '0' AND c = ? IF EXISTS"); - // static column, only the applied CAS updates can fire the guardrail + // static column, both failed and applied CAS updates should fire the guardrail assertValid("INSERT INTO %s (k, s) VALUES ('0', '0')"); testThreshold("s", "UPDATE %s SET s = ? WHERE k = '0' IF EXISTS"); assertValid("DELETE FROM %s WHERE k = '0'"); - testNoThreshold("UPDATE %s SET s = ? WHERE k = '0' IF EXISTS"); + testThreshold("s", "UPDATE %s SET s = ? WHERE k = '0' IF EXISTS"); - // regular column, only the applied CAS updates can fire the guardrail + // regular column, both failed and applied CAS updates should fire the guardrail assertValid("INSERT INTO %s (k, c) VALUES ('0', '0')"); testThreshold("v", "UPDATE %s SET v = ? WHERE k = '0' AND c = '0' IF EXISTS"); assertValid("DELETE FROM %s WHERE k = '0' AND c = '0'"); - testNoThreshold("UPDATE %s SET v = ? WHERE k = '0' AND c = '0' IF EXISTS"); + testThreshold("v", "UPDATE %s SET v = ? WHERE k = '0' AND c = '0' IF EXISTS"); } @Test @@ -240,9 +240,9 @@ public abstract class ColumnTypeSpecificValueThresholdTester extends ValueThresh assertValid("UPDATE %s SET v = '0' WHERE k = 0"); assertValid("UPDATE %s SET v = ? WHERE k = 0 IF v = '0'", allocate(WARN_THRESHOLD)); - // updates beyond the threshold fail only if the update is applied + // updates beyond the threshold always warn, independently of whether they are applied assertValid("DELETE FROM %s WHERE k = 0"); - assertValid("UPDATE %s SET v = ? WHERE k = 0 IF v = '0'", allocate(WARN_THRESHOLD + 1)); + assertWarns("v", "UPDATE %s SET v = ? WHERE k = 0 IF v = '0'", allocate(WARN_THRESHOLD + 1)); assertValid("UPDATE %s SET v = '0' WHERE k = 0"); assertWarns("v", "UPDATE %s SET v = ? WHERE k = 0 IF v = '0'", allocate(WARN_THRESHOLD + 1)); } diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailColumnBlobValueSizeTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailColumnBlobValueSizeTest.java index 12e5ca2395..1470fb02cd 100644 --- a/test/unit/org/apache/cassandra/db/guardrails/GuardrailColumnBlobValueSizeTest.java +++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailColumnBlobValueSizeTest.java @@ -105,14 +105,14 @@ public class GuardrailColumnBlobValueSizeTest extends ColumnTypeSpecificValueThr // static column assertValid("INSERT INTO %s (k, s) VALUES ('1', ?) IF NOT EXISTS", allocate(1)); assertValid("INSERT INTO %s (k, s) VALUES ('2', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD)); - assertValid("INSERT INTO %s (k, s) VALUES ('2', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); // not applied - assertWarns("s", "INSERT INTO %s (k, s) VALUES ('3', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); + assertWarns("s", "INSERT INTO %s (k, s) VALUES ('2', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); // not applied + assertWarns("s", "INSERT INTO %s (k, s) VALUES ('3', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); // applied // regular column assertValid("INSERT INTO %s (k, c, v) VALUES ('4', textAsBlob('0'), ?) IF NOT EXISTS", allocate(1)); assertValid("INSERT INTO %s (k, c, v) VALUES ('5', textAsBlob('0'), ?) IF NOT EXISTS", allocate(WARN_THRESHOLD)); - assertValid("INSERT INTO %s (k, c, v) VALUES ('5', textAsBlob('0'), ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); // not applied - assertWarns("v", "INSERT INTO %s (k, c, v) VALUES ('6', textAsBlob('0'), ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); + assertWarns("v", "INSERT INTO %s (k, c, v) VALUES ('5', textAsBlob('0'), ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); // not applied + assertWarns("v", "INSERT INTO %s (k, c, v) VALUES ('6', textAsBlob('0'), ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); // applied } @Override @@ -127,17 +127,17 @@ public class GuardrailColumnBlobValueSizeTest extends ColumnTypeSpecificValueThr // clustering key, the CAS updates with values beyond the threshold are not applied, so they don't come to fail testNoThreshold("UPDATE %s SET v = textAsBlob('0') WHERE k = '0' AND c = ? IF EXISTS"); - // static column, only the applied CAS updates can fire the guardrail + // static column, successful and failed updates should fire the guardrail assertValid("INSERT INTO %s (k, s) VALUES ('0', textAsBlob('0'))"); testThreshold("s", "UPDATE %s SET s = ? WHERE k = '0' IF EXISTS"); assertValid("DELETE FROM %s WHERE k = '0'"); - testNoThreshold("UPDATE %s SET s = ? WHERE k = '0' IF EXISTS"); + testThreshold("s", "UPDATE %s SET s = ? WHERE k = '0' IF EXISTS"); - // regular column, only the applied CAS updates can fire the guardrail + // regular column, successful and failed updates should fire the guardrail assertValid("INSERT INTO %s (k, c) VALUES ('0', textAsBlob('0'))"); testThreshold("v", "UPDATE %s SET v = ? WHERE k = '0' AND c = textAsBlob('0') IF EXISTS"); assertValid("DELETE FROM %s WHERE k = '0' AND c = textAsBlob('0')"); - testNoThreshold("UPDATE %s SET v = ? WHERE k = '0' AND c = textAsBlob('0') IF EXISTS"); + testThreshold("v", "UPDATE %s SET v = ? WHERE k = '0' AND c = textAsBlob('0') IF EXISTS"); } @Test @@ -157,9 +157,9 @@ public class GuardrailColumnBlobValueSizeTest extends ColumnTypeSpecificValueThr assertValid("UPDATE %s SET v = textAsBlob('0') WHERE k = 0"); assertValid("UPDATE %s SET v = ? WHERE k = 0 IF v = textAsBlob('0')", allocate(WARN_THRESHOLD)); - // updates beyond the threshold fail only if the update is applied + // updates beyond the threshold fail, independently of whether they are applied assertValid("DELETE FROM %s WHERE k = 0"); - assertValid("UPDATE %s SET v = ? WHERE k = 0 IF v = textAsBlob('0')", allocate(WARN_THRESHOLD + 1)); + assertWarns("v", "UPDATE %s SET v = ? WHERE k = 0 IF v = textAsBlob('0')", allocate(WARN_THRESHOLD + 1)); assertValid("UPDATE %s SET v = textAsBlob('0') WHERE k = 0"); assertWarns("v", "UPDATE %s SET v = ? WHERE k = 0 IF v = textAsBlob('0')", allocate(WARN_THRESHOLD + 1)); } diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailColumnValueSizeTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailColumnValueSizeTest.java index 7827508dd4..fd0ca735d6 100644 --- a/test/unit/org/apache/cassandra/db/guardrails/GuardrailColumnValueSizeTest.java +++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailColumnValueSizeTest.java @@ -373,14 +373,14 @@ public class GuardrailColumnValueSizeTest extends ValueThresholdTester // static column assertValid("INSERT INTO %s (k, s) VALUES ('1', ?) IF NOT EXISTS", allocate(1)); assertValid("INSERT INTO %s (k, s) VALUES ('2', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD)); - assertValid("INSERT INTO %s (k, s) VALUES ('2', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); // not applied - assertWarns("s", "INSERT INTO %s (k, s) VALUES ('3', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); + assertWarns("s", "INSERT INTO %s (k, s) VALUES ('2', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); // not applied + assertWarns("s", "INSERT INTO %s (k, s) VALUES ('3', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); // applied // regular column assertValid("INSERT INTO %s (k, c, v) VALUES ('4', '0', ?) IF NOT EXISTS", allocate(1)); assertValid("INSERT INTO %s (k, c, v) VALUES ('5', '0', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD)); - assertValid("INSERT INTO %s (k, c, v) VALUES ('5', '0', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); // not applied - assertWarns("v", "INSERT INTO %s (k, c, v) VALUES ('6', '0', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); + assertWarns("v", "INSERT INTO %s (k, c, v) VALUES ('5', '0', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); // not applied + assertWarns("v", "INSERT INTO %s (k, c, v) VALUES ('6', '0', ?) IF NOT EXISTS", allocate(WARN_THRESHOLD + 1)); // applied } @Test @@ -394,17 +394,17 @@ public class GuardrailColumnValueSizeTest extends ValueThresholdTester // clustering key, the CAS updates with values beyond the threshold are not applied, so they don't come to fail testNoThreshold("UPDATE %s SET v = '0' WHERE k = '0' AND c = ? IF EXISTS"); - // static column, only the applied CAS updates can fire the guardrail + // static column, the guardrail fires regardless of whether the CAS update is applied assertValid("INSERT INTO %s (k, s) VALUES ('0', '0')"); testThreshold("s", "UPDATE %s SET s = ? WHERE k = '0' IF EXISTS"); assertValid("DELETE FROM %s WHERE k = '0'"); - testNoThreshold("UPDATE %s SET s = ? WHERE k = '0' IF EXISTS"); + testThreshold("s", "UPDATE %s SET s = ? WHERE k = '0' IF EXISTS"); - // regular column, only the applied CAS updates can fire the guardrail + // regular column, the guardrail fires regardless of whether the CAS update is applied assertValid("INSERT INTO %s (k, c) VALUES ('0', '0')"); testThreshold("v", "UPDATE %s SET v = ? WHERE k = '0' AND c = '0' IF EXISTS"); assertValid("DELETE FROM %s WHERE k = '0' AND c = '0'"); - testNoThreshold("UPDATE %s SET v = ? WHERE k = '0' AND c = '0' IF EXISTS"); + testThreshold("v", "UPDATE %s SET v = ? WHERE k = '0' AND c = '0' IF EXISTS"); } @Test @@ -424,9 +424,9 @@ public class GuardrailColumnValueSizeTest extends ValueThresholdTester assertValid("UPDATE %s SET v = '0' WHERE k = 0"); assertValid("UPDATE %s SET v = ? WHERE k = 0 IF v = '0'", allocate(WARN_THRESHOLD)); - // updates beyond the threshold fail only if the update is applied + // updates beyond the threshold fire the guardrail regardless of whether the update is applied assertValid("DELETE FROM %s WHERE k = 0"); - assertValid("UPDATE %s SET v = ? WHERE k = 0 IF v = '0'", allocate(WARN_THRESHOLD + 1)); + assertWarns("v", "UPDATE %s SET v = ? WHERE k = 0 IF v = '0'", allocate(WARN_THRESHOLD + 1)); assertValid("UPDATE %s SET v = '0' WHERE k = 0"); assertWarns("v", "UPDATE %s SET v = ? WHERE k = 0 IF v = '0'", allocate(WARN_THRESHOLD + 1)); } diff --git a/test/unit/org/apache/cassandra/exceptions/CassandraExceptionSerializationTest.java b/test/unit/org/apache/cassandra/exceptions/CassandraExceptionSerializationTest.java new file mode 100644 index 0000000000..b012e416ce --- /dev/null +++ b/test/unit/org/apache/cassandra/exceptions/CassandraExceptionSerializationTest.java @@ -0,0 +1,1094 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.exceptions; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import org.apache.cassandra.cql3.constraints.ConstraintViolationException; +import org.apache.cassandra.cql3.constraints.InvalidConstraintDefinitionException; +import org.apache.cassandra.cql3.functions.FunctionName; +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.KeyspaceNotDefinedException; +import org.apache.cassandra.db.MutationExceededMaxSizeException; +import org.apache.cassandra.db.WriteType; +import org.apache.cassandra.db.guardrails.GuardrailViolatedException; +import org.apache.cassandra.io.util.DataInputBuffer; +import org.apache.cassandra.io.util.DataOutputBuffer; +import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.service.accord.exceptions.AccordReadExhaustedException; +import org.apache.cassandra.service.accord.exceptions.AccordReadPreemptedException; +import org.apache.cassandra.service.accord.exceptions.AccordWriteExhaustedException; +import org.apache.cassandra.service.accord.exceptions.AccordWritePreemptedException; +import org.apache.cassandra.triggers.TriggerDisabledException; +import org.apache.cassandra.utils.MD5Digest; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * Comprehensive tests for CassandraException serialization. + * Tests that all exception types properly serialize/deserialize with: + * - Correct type fidelity (no type loss during roundtrip) + * - All exception-specific fields preserved + * - Stack traces, causes, and suppressed exceptions preserved + */ +public class CassandraExceptionSerializationTest +{ + private static final int VERSION = MessagingService.current_version; + + @Test + public void testUnavailableException() throws IOException + { + ConsistencyLevel cl = ConsistencyLevel.QUORUM; + int required = 3; + int alive = 1; + + UnavailableException original = UnavailableException.create(cl, required, alive); + + // Add cause and stack trace + RuntimeException cause = new RuntimeException("Test cause"); + original.initCause(cause); + original.addSuppressed(new IllegalArgumentException("Suppressed exception")); + + UnavailableException deserialized = roundTrip(original); + + // Verify type fidelity + assertEquals(UnavailableException.class, deserialized.getClass()); + + // Verify specific fields + assertEquals(cl, deserialized.consistency); + assertEquals(required, deserialized.required); + assertEquals(alive, deserialized.alive); + assertNotNull(deserialized.getMessage()); + + // Verify cause and suppressed exceptions are preserved (now as RemoteExceptions with host prefix) + assertNotNull(deserialized.getCause()); + assertTrue("Cause should contain 'Test cause'", deserialized.getCause().getMessage().contains("Test cause")); + assertEquals(1, deserialized.getSuppressed().length); + assertTrue("Suppressed should contain 'Suppressed exception'", + deserialized.getSuppressed()[0].getMessage().contains("Suppressed exception")); + + // Verify stack trace is preserved + assertTrue(deserialized.getStackTrace().length > 0); + } + + @Test + public void testWriteFailureException() throws IOException + { + ConsistencyLevel cl = ConsistencyLevel.QUORUM; + int received = 2; + int blockFor = 3; + WriteType writeType = WriteType.SIMPLE; + Map failures = new HashMap<>(); + InetAddressAndPort addr1 = InetAddressAndPort.getByName("127.0.0.1"); + InetAddressAndPort addr2 = InetAddressAndPort.getByName("127.0.0.2"); + failures.put(addr1, RequestFailureReason.TIMEOUT); + failures.put(addr2, RequestFailureReason.UNKNOWN); + + WriteFailureException original = new WriteFailureException(cl, received, blockFor, writeType, failures); + + WriteFailureException deserialized = roundTrip(original); + + // Verify type fidelity + assertEquals(WriteFailureException.class, deserialized.getClass()); + + // Verify specific fields + assertEquals(cl, deserialized.consistency); + assertEquals(received, deserialized.received); + assertEquals(blockFor, deserialized.blockFor); + assertEquals(writeType, deserialized.writeType); + assertEquals(failures.size(), deserialized.failureReasonByEndpoint.size()); + assertEquals(failures.get(addr1), deserialized.failureReasonByEndpoint.get(addr1)); + assertEquals(failures.get(addr2), deserialized.failureReasonByEndpoint.get(addr2)); + } + + @Test + public void testReadFailureException() throws IOException + { + ConsistencyLevel cl = ConsistencyLevel.QUORUM; + int received = 2; + int blockFor = 3; + boolean dataPresent = true; + Map failures = new HashMap<>(); + InetAddressAndPort addr = InetAddressAndPort.getByName("127.0.0.1"); + failures.put(addr, RequestFailureReason.TIMEOUT); + + ReadFailureException original = new ReadFailureException(cl, received, blockFor, dataPresent, failures); + + ReadFailureException deserialized = roundTrip(original); + + // Verify type fidelity + assertEquals(ReadFailureException.class, deserialized.getClass()); + + // Verify specific fields + assertEquals(cl, deserialized.consistency); + assertEquals(received, deserialized.received); + assertEquals(blockFor, deserialized.blockFor); + assertEquals(dataPresent, deserialized.dataPresent); + assertEquals(failures.size(), deserialized.failureReasonByEndpoint.size()); + assertEquals(failures.get(addr), deserialized.failureReasonByEndpoint.get(addr)); + } + + @Test + public void testTombstoneAbortException() throws IOException + { + ConsistencyLevel cl = ConsistencyLevel.ONE; + int received = 1; + int blockFor = 1; + boolean dataPresent = true; + int nodes = 3; + long tombstones = 100000L; + Map failures = new HashMap<>(); + InetAddressAndPort addr = InetAddressAndPort.getByName("127.0.0.1"); + failures.put(addr, RequestFailureReason.READ_TOO_MANY_TOMBSTONES); + + TombstoneAbortException original = new TombstoneAbortException( + "Too many tombstones", nodes, tombstones, dataPresent, cl, received, blockFor, failures); + + TombstoneAbortException deserialized = roundTrip(original); + + // Verify type fidelity - this should now preserve the exact type + assertEquals(TombstoneAbortException.class, deserialized.getClass()); + + // Verify specific fields + assertEquals(cl, deserialized.consistency); + assertEquals(received, deserialized.received); + assertEquals(blockFor, deserialized.blockFor); + assertEquals(dataPresent, deserialized.dataPresent); + assertEquals(nodes, deserialized.nodes); + assertEquals(tombstones, deserialized.tombstones); + assertEquals(failures.size(), deserialized.failureReasonByEndpoint.size()); + assertEquals(failures.get(addr), deserialized.failureReasonByEndpoint.get(addr)); + } + + @Test + public void testReadSizeAbortException() throws IOException + { + ConsistencyLevel cl = ConsistencyLevel.LOCAL_ONE; + int received = 1; + int blockFor = 1; + boolean dataPresent = true; + Map failures = new HashMap<>(); + InetAddressAndPort addr = InetAddressAndPort.getByName("127.0.0.1"); + failures.put(addr, RequestFailureReason.READ_SIZE); + + ReadSizeAbortException original = new ReadSizeAbortException( + "Read size too large", cl, received, blockFor, dataPresent, failures); + + ReadSizeAbortException deserialized = roundTrip(original); + + // Verify type fidelity - this should now preserve the exact type + assertEquals(ReadSizeAbortException.class, deserialized.getClass()); + + // Verify specific fields + assertEquals(cl, deserialized.consistency); + assertEquals(received, deserialized.received); + assertEquals(blockFor, deserialized.blockFor); + assertEquals(dataPresent, deserialized.dataPresent); + assertEquals(failures.size(), deserialized.failureReasonByEndpoint.size()); + assertEquals(failures.get(addr), deserialized.failureReasonByEndpoint.get(addr)); + } + + @Test + public void testQueryReferencesTooManyIndexesAbortException() throws IOException + { + ConsistencyLevel cl = ConsistencyLevel.QUORUM; + int received = 2; + int blockFor = 2; + boolean dataPresent = true; + int nodes = 2; + long maxValue = 5L; + Map failures = new HashMap<>(); + InetAddressAndPort addr = InetAddressAndPort.getByName("127.0.0.1"); + failures.put(addr, RequestFailureReason.INDEX_NOT_AVAILABLE); + + QueryReferencesTooManyIndexesAbortException original = new QueryReferencesTooManyIndexesAbortException( + "Too many indexes", nodes, maxValue, dataPresent, cl, received, blockFor, failures); + + QueryReferencesTooManyIndexesAbortException deserialized = roundTrip(original); + + // Verify type fidelity - this should now preserve the exact type + assertEquals(QueryReferencesTooManyIndexesAbortException.class, deserialized.getClass()); + + // Verify specific fields + assertEquals(cl, deserialized.consistency); + assertEquals(received, deserialized.received); + assertEquals(blockFor, deserialized.blockFor); + assertEquals(dataPresent, deserialized.dataPresent); + assertEquals(nodes, deserialized.nodes); + assertEquals(maxValue, deserialized.maxValue); + assertEquals(failures.size(), deserialized.failureReasonByEndpoint.size()); + assertEquals(failures.get(addr), deserialized.failureReasonByEndpoint.get(addr)); + } + + @Test + public void testOverloadedException() throws IOException + { + String message = "Server overloaded"; + OverloadedException original = new OverloadedException(message); + + OverloadedException deserialized = roundTrip(original); + + // Verify type fidelity - this should now preserve the exact type instead of becoming InvalidRequestException + assertEquals(OverloadedException.class, deserialized.getClass()); + assertEquals(message, deserialized.getMessage()); + } + + @Test + public void testTruncateException() throws IOException + { + String message = "Truncate failed due to timeout"; + TruncateException original = new TruncateException(message); + + TruncateException deserialized = roundTrip(original); + + // Verify type fidelity - this should now preserve the exact type instead of becoming InvalidRequestException + assertEquals(TruncateException.class, deserialized.getClass()); + assertEquals(message, deserialized.getMessage()); + } + + @Test + public void testAuthenticationException() throws IOException + { + String message = "Authentication failed"; + AuthenticationException original = new AuthenticationException(message); + + AuthenticationException deserialized = roundTrip(original); + + // Verify type fidelity - this should now preserve the exact type instead of becoming InvalidRequestException + assertEquals(AuthenticationException.class, deserialized.getClass()); + assertEquals(message, deserialized.getMessage()); + } + + @Test + public void testFunctionExecutionException() throws IOException + { + FunctionName functionName = new FunctionName("test_ks", "test_fn"); + List argTypes = Arrays.asList("int", "text"); + String detail = "division by zero"; + + FunctionExecutionException original = FunctionExecutionException.create(functionName, argTypes, detail); + FunctionExecutionException deserialized = roundTrip(original); + + assertEquals(FunctionExecutionException.class, deserialized.getClass()); + assertEquals(functionName.keyspace, deserialized.functionName.keyspace); + assertEquals(functionName.name, deserialized.functionName.name); + assertEquals(argTypes, deserialized.argTypes); + assertEquals(original.detail, deserialized.detail); + } + + @Test + public void testOperationExecutionException() throws IOException + { + List argTypes = Arrays.asList("int", "int"); + String detail = "Division by zero"; + + OperationExecutionException original = new OperationExecutionException('/', argTypes, detail); + + OperationExecutionException deserialized = roundTrip(original); + + // Verify type fidelity - should remain OperationExecutionException, not become FunctionExecutionException + assertEquals(OperationExecutionException.class, deserialized.getClass()); + + // '/' operator maps to function name "_divide" + assertEquals("_divide", deserialized.functionName.name); + assertEquals(argTypes, deserialized.argTypes); + assertTrue(deserialized.detail.contains(detail)); + } + + @Test + public void testNestedExceptionSerialization() throws IOException + { + // Create a complex nested exception scenario + InvalidRequestException original = new InvalidRequestException("Root error"); + + // Add cause chain + RuntimeException cause1 = new RuntimeException("Cause level 1"); + IllegalArgumentException cause2 = new IllegalArgumentException("Cause level 2"); + cause1.initCause(cause2); + original.initCause(cause1); + + // Add suppressed exceptions + original.addSuppressed(new IllegalStateException("Suppressed 1")); + original.addSuppressed(new UnsupportedOperationException("Suppressed 2")); + + // Set stack trace + StackTraceElement[] stackTrace = { + new StackTraceElement("TestClass", "testMethod", "TestClass.java", 100), + new StackTraceElement("AnotherClass", "anotherMethod", "AnotherClass.java", 200) + }; + original.setStackTrace(stackTrace); + + InvalidRequestException deserialized = roundTrip(original); + + // Verify type fidelity + assertEquals(InvalidRequestException.class, deserialized.getClass()); + assertEquals("Root error", deserialized.getMessage()); + + // Verify cause chain (now using RemoteException format) + assertNotNull(deserialized.getCause()); + assertTrue("Cause should contain 'Cause level 1'", deserialized.getCause().getMessage().contains("Cause level 1")); + assertNotNull(deserialized.getCause().getCause()); + assertTrue("Nested cause should contain 'Cause level 2'", deserialized.getCause().getCause().getMessage().contains("Cause level 2")); + + // Verify suppressed exceptions (now using RemoteException format) + assertEquals(2, deserialized.getSuppressed().length); + assertTrue("Suppressed 1 should contain message", deserialized.getSuppressed()[0].getMessage().contains("Suppressed 1")); + assertTrue("Suppressed 2 should contain message", deserialized.getSuppressed()[1].getMessage().contains("Suppressed 2")); + + // Verify stack trace + assertEquals(2, deserialized.getStackTrace().length); + assertEquals("TestClass", deserialized.getStackTrace()[0].getClassName()); + assertEquals("testMethod", deserialized.getStackTrace()[0].getMethodName()); + assertEquals("TestClass.java", deserialized.getStackTrace()[0].getFileName()); + assertEquals(100, deserialized.getStackTrace()[0].getLineNumber()); + } + + @Test + public void testReadTimeoutException() throws IOException + { + ConsistencyLevel cl = ConsistencyLevel.QUORUM; + int received = 2; + int blockFor = 3; + boolean dataPresent = true; + + ReadTimeoutException original = new ReadTimeoutException(cl, received, blockFor, dataPresent); + + ReadTimeoutException deserialized = roundTrip(original); + + // Verify type fidelity + assertEquals(ReadTimeoutException.class, deserialized.getClass()); + + // Verify specific fields + assertEquals(cl, deserialized.consistency); + assertEquals(received, deserialized.received); + assertEquals(blockFor, deserialized.blockFor); + assertEquals(dataPresent, deserialized.dataPresent); + assertNotNull(deserialized.getMessage()); + } + + @Test + public void testWriteTimeoutException() throws IOException + { + WriteType writeType = WriteType.SIMPLE; + ConsistencyLevel cl = ConsistencyLevel.LOCAL_QUORUM; + int received = 1; + int blockFor = 2; + + WriteTimeoutException original = new WriteTimeoutException(writeType, cl, received, blockFor); + + WriteTimeoutException deserialized = roundTrip(original); + + // Verify type fidelity + assertEquals(WriteTimeoutException.class, deserialized.getClass()); + + // Verify specific fields + assertEquals(cl, deserialized.consistency); + assertEquals(received, deserialized.received); + assertEquals(blockFor, deserialized.blockFor); + assertEquals(writeType, deserialized.writeType); + assertNotNull(deserialized.getMessage()); + } + + @Test + public void testCasWriteTimeoutException() throws IOException + { + WriteType writeType = WriteType.CAS; + ConsistencyLevel cl = ConsistencyLevel.SERIAL; + int received = 1; + int blockFor = 3; + int contentions = 5; + + CasWriteTimeoutException original = new CasWriteTimeoutException(writeType, cl, received, blockFor, contentions); + + CasWriteTimeoutException deserialized = roundTrip(original); + + // Verify type fidelity + assertEquals(CasWriteTimeoutException.class, deserialized.getClass()); + + // Verify specific fields + assertEquals(cl, deserialized.consistency); + assertEquals(received, deserialized.received); + assertEquals(blockFor, deserialized.blockFor); + assertEquals(writeType, deserialized.writeType); + assertEquals(contentions, deserialized.contentions); + assertNotNull(deserialized.getMessage()); + } + + @Test + public void testAccordReadExhaustedException() throws IOException + { + int received = 1; + int blockFor = 2; + boolean dataPresent = false; + + AccordReadExhaustedException original = new AccordReadExhaustedException(received, blockFor, dataPresent); + + AccordReadExhaustedException deserialized = roundTrip(original); + + // Verify type fidelity + assertEquals(AccordReadExhaustedException.class, deserialized.getClass()); + + // Verify specific fields (these extend ReadTimeoutException) + assertEquals(ConsistencyLevel.SERIAL, deserialized.consistency); // Accord exceptions use SERIAL + assertEquals(received, deserialized.received); + assertEquals(blockFor, deserialized.blockFor); + assertEquals(dataPresent, deserialized.dataPresent); + assertNotNull(deserialized.getMessage()); + } + + @Test + public void testAccordReadPreemptedException() throws IOException + { + int received = 0; + int blockFor = 3; + boolean dataPresent = true; + + AccordReadPreemptedException original = new AccordReadPreemptedException(received, blockFor, dataPresent); + + AccordReadPreemptedException deserialized = roundTrip(original); + + // Verify type fidelity + assertEquals(AccordReadPreemptedException.class, deserialized.getClass()); + + // Verify specific fields + assertEquals(ConsistencyLevel.SERIAL, deserialized.consistency); + assertEquals(received, deserialized.received); + assertEquals(blockFor, deserialized.blockFor); + assertEquals(dataPresent, deserialized.dataPresent); + assertNotNull(deserialized.getMessage()); + } + + @Test + public void testAccordWriteExhaustedException() throws IOException + { + int received = 1; + int blockFor = 2; + + AccordWriteExhaustedException original = new AccordWriteExhaustedException(received, blockFor); + + AccordWriteExhaustedException deserialized = roundTrip(original); + + // Verify type fidelity + assertEquals(AccordWriteExhaustedException.class, deserialized.getClass()); + + // Verify specific fields (these extend WriteTimeoutException) + assertEquals(ConsistencyLevel.SERIAL, deserialized.consistency); + assertEquals(received, deserialized.received); + assertEquals(blockFor, deserialized.blockFor); + assertNotNull(deserialized.getMessage()); + } + + @Test + public void testAccordWritePreemptedException() throws IOException + { + int received = 0; + int blockFor = 3; + + AccordWritePreemptedException original = new AccordWritePreemptedException(received, blockFor); + + AccordWritePreemptedException deserialized = roundTrip(original); + + // Verify type fidelity + assertEquals(AccordWritePreemptedException.class, deserialized.getClass()); + + // Verify specific fields + assertEquals(ConsistencyLevel.SERIAL, deserialized.consistency); + assertEquals(received, deserialized.received); + assertEquals(blockFor, deserialized.blockFor); + assertNotNull(deserialized.getMessage()); + } + + @Test + public void testConstraintViolationException() throws IOException + { + String message = "Constraint violated: value out of range"; + ConstraintViolationException original = new ConstraintViolationException(message); + + ConstraintViolationException deserialized = roundTrip(original); + + // Verify type fidelity + assertEquals(ConstraintViolationException.class, deserialized.getClass()); + assertEquals(message, deserialized.getMessage()); + } + + @Test + public void testInvalidConstraintDefinitionException() throws IOException + { + String message = "Invalid constraint definition: syntax error"; + InvalidConstraintDefinitionException original = new InvalidConstraintDefinitionException(message); + + InvalidConstraintDefinitionException deserialized = roundTrip(original); + + // Verify type fidelity + assertEquals(InvalidConstraintDefinitionException.class, deserialized.getClass()); + assertEquals(message, deserialized.getMessage()); + } + + @Test + public void testTriggerDisabledException() throws IOException + { + String message = "Trigger is disabled"; + TriggerDisabledException original = new TriggerDisabledException(message); + + TriggerDisabledException deserialized = roundTrip(original); + + // Verify type fidelity + assertEquals(TriggerDisabledException.class, deserialized.getClass()); + assertEquals(message, deserialized.getMessage()); + } + + @Test + public void testKeyspaceNotDefinedException() throws IOException + { + String message = "Keyspace 'test_ks' does not exist"; + KeyspaceNotDefinedException original = new KeyspaceNotDefinedException(message); + + KeyspaceNotDefinedException deserialized = roundTrip(original); + + // Verify type fidelity + assertEquals(KeyspaceNotDefinedException.class, deserialized.getClass()); + assertEquals(message, deserialized.getMessage()); + } + + @Test + public void testGuardrailViolatedException() throws IOException + { + String message = "Query exceeded guardrail: too many tombstones"; + GuardrailViolatedException original = new GuardrailViolatedException(message); + + GuardrailViolatedException deserialized = roundTrip(original); + + // Verify type fidelity + assertEquals(GuardrailViolatedException.class, deserialized.getClass()); + assertEquals(message, deserialized.getMessage()); + } + + @Test + public void testMutationExceededMaxSizeException() throws IOException + { + String message = "Mutation size exceeded maximum: 16MB/12MB for keyspace test_ks"; + long mutationSize = 16777216L; // 16MB + + MutationExceededMaxSizeException original = new MutationExceededMaxSizeException(message, mutationSize); + + MutationExceededMaxSizeException deserialized = roundTrip(original); + + // Verify type fidelity + assertEquals(MutationExceededMaxSizeException.class, deserialized.getClass()); + assertEquals(message, deserialized.getMessage()); + assertEquals(mutationSize, deserialized.mutationSize); + } + + @Test + public void testAllSimpleExceptionTypes() throws IOException + { + // Test all simple exception types that should roundtrip correctly + testSimpleException(new SyntaxException("Syntax error")); + testSimpleException(new UnauthorizedException("Not authorized")); + testSimpleException(new InvalidRequestException("Invalid request")); + testSimpleException(new ConfigurationException("Config error")); + testSimpleException(new CDCWriteException("CDC write failed")); + testSimpleException(new IsBootstrappingException()); + testSimpleException(new OversizedCQLMessageException("Message too large")); + testSimpleException(new InvalidRoutingException("Invalid routing")); + } + + private void testSimpleException(CassandraException original) throws IOException + { + CassandraException deserialized = roundTrip(original); + + // Verify type fidelity + assertEquals("Type should be preserved for " + original.getClass().getSimpleName(), + original.getClass(), deserialized.getClass()); + + // Verify message preservation + assertEquals("Message should be preserved for " + original.getClass().getSimpleName(), + original.getMessage(), deserialized.getMessage()); + + // Verify legacy code is preserved for protocol compatibility + assertEquals("Legacy ExceptionCode should be preserved for " + original.getClass().getSimpleName(), + original.code(), deserialized.code()); + } + + /** + * Helper method to serialize and deserialize an exception + */ + @SuppressWarnings("unchecked") + private T roundTrip(T original) throws IOException + { + DataOutputBuffer out = new DataOutputBuffer(); + CassandraException.serializer.serialize(original, out, VERSION); + + // Verify serialized size matches actual size + long expectedSize = CassandraException.serializer.serializedSize(original, VERSION); + assertEquals("Serialized size mismatch for " + original.getClass().getSimpleName(), + expectedSize, out.getLength()); + + DataInputBuffer in = new DataInputBuffer(out.toByteArray()); + return (T) CassandraException.serializer.deserialize(in, VERSION); + } + + // Edge Case Tests + + @Test + public void testNullAndEmptyMessages() throws IOException + { + // Test null message + InvalidRequestException nullMessage = new InvalidRequestException(null); + InvalidRequestException deserializedNull = roundTrip(nullMessage); + assertEquals(InvalidRequestException.class, deserializedNull.getClass()); + // Null messages get converted to empty strings in serialization + assertEquals("", deserializedNull.getMessage()); + + // Test empty message + InvalidRequestException emptyMessage = new InvalidRequestException(""); + InvalidRequestException deserializedEmpty = roundTrip(emptyMessage); + assertEquals(InvalidRequestException.class, deserializedEmpty.getClass()); + assertEquals("", deserializedEmpty.getMessage()); + } + + @Test + public void testUnicodeMessages() throws IOException + { + // Test various Unicode characters + String unicodeMessage = "Error: 中文测试 🔥 Ω α β γ 🚀 Ñoël"; + InvalidRequestException original = new InvalidRequestException(unicodeMessage); + + InvalidRequestException deserialized = roundTrip(original); + + assertEquals(InvalidRequestException.class, deserialized.getClass()); + assertEquals(unicodeMessage, deserialized.getMessage()); + } + + @Test + public void testVeryLargeMessages() throws IOException + { + // Test large message (16KB - within TypeSizes limits) + StringBuilder largeMessage = new StringBuilder(); + for (int i = 0; i < 16384; i++) + { + largeMessage.append("A"); + } + + InvalidRequestException original = new InvalidRequestException(largeMessage.toString()); + InvalidRequestException deserialized = roundTrip(original); + + assertEquals(InvalidRequestException.class, deserialized.getClass()); + assertEquals(largeMessage.toString(), deserialized.getMessage()); + } + + @Test + public void testExceptionWithNullStackTrace() throws IOException + { + // Note: Java doesn't allow setting null stack trace, so we test empty instead + InvalidRequestException original = new InvalidRequestException("Test message"); + original.setStackTrace(new StackTraceElement[0]); + + InvalidRequestException deserialized = roundTrip(original); + + assertEquals(InvalidRequestException.class, deserialized.getClass()); + assertEquals("Test message", deserialized.getMessage()); + // Empty stack trace should be preserved + assertEquals(0, deserialized.getStackTrace().length); + } + + @Test + public void testStackTraceWithNullElements() throws IOException + { + InvalidRequestException original = new InvalidRequestException("Test message"); + + // Stack trace with null file name and negative line number + StackTraceElement[] stackTrace = { + new StackTraceElement("TestClass", "testMethod", null, -1), + new StackTraceElement("AnotherClass", "anotherMethod", "AnotherClass.java", 0) + }; + original.setStackTrace(stackTrace); + + InvalidRequestException deserialized = roundTrip(original); + + assertEquals(InvalidRequestException.class, deserialized.getClass()); + assertEquals(2, deserialized.getStackTrace().length); + + StackTraceElement elem1 = deserialized.getStackTrace()[0]; + assertEquals("TestClass", elem1.getClassName()); + assertEquals("testMethod", elem1.getMethodName()); + assertNull(elem1.getFileName()); + assertEquals(-1, elem1.getLineNumber()); + } + + @Test + public void testExceptionWithoutCauseOrSuppressed() throws IOException + { + InvalidRequestException original = new InvalidRequestException("Test message"); + // Explicitly ensure no cause or suppressed exceptions + + InvalidRequestException deserialized = roundTrip(original); + + assertEquals(InvalidRequestException.class, deserialized.getClass()); + assertEquals("Test message", deserialized.getMessage()); + assertNull(deserialized.getCause()); + assertEquals(0, deserialized.getSuppressed().length); + } + + @Test + public void testUnavailableExceptionEdgeCases() throws IOException + { + // Test with zero alive, one required + UnavailableException zeroValues = UnavailableException.create(ConsistencyLevel.ONE, 1, 0); + UnavailableException deserializedZero = roundTrip(zeroValues); + + assertEquals(UnavailableException.class, deserializedZero.getClass()); + assertEquals(ConsistencyLevel.ONE, deserializedZero.consistency); + assertEquals(1, deserializedZero.required); + assertEquals(0, deserializedZero.alive); + + // Test with very large values + UnavailableException largeValues = UnavailableException.create(ConsistencyLevel.ALL, Integer.MAX_VALUE, Integer.MAX_VALUE - 1); + UnavailableException deserializedLarge = roundTrip(largeValues); + + assertEquals(UnavailableException.class, deserializedLarge.getClass()); + assertEquals(ConsistencyLevel.ALL, deserializedLarge.consistency); + assertEquals(Integer.MAX_VALUE, deserializedLarge.required); + assertEquals(Integer.MAX_VALUE - 1, deserializedLarge.alive); + } + + @Test + public void testWriteFailureExceptionWithEmptyFailuresMap() throws IOException + { + ConsistencyLevel cl = ConsistencyLevel.ONE; + int received = 1; + int blockFor = 1; + WriteType writeType = WriteType.SIMPLE; + Map emptyFailures = new HashMap<>(); + + WriteFailureException original = new WriteFailureException(cl, received, blockFor, writeType, emptyFailures); + WriteFailureException deserialized = roundTrip(original); + + assertEquals(WriteFailureException.class, deserialized.getClass()); + assertEquals(cl, deserialized.consistency); + assertEquals(received, deserialized.received); + assertEquals(blockFor, deserialized.blockFor); + assertEquals(writeType, deserialized.writeType); + assertEquals(0, deserialized.failureReasonByEndpoint.size()); + } + + @Test + public void testTombstoneAbortExceptionExtremValues() throws IOException + { + ConsistencyLevel cl = ConsistencyLevel.ONE; + int received = 1; + int blockFor = 1; + boolean dataPresent = true; + + // Test with extreme values + int maxNodes = Integer.MAX_VALUE; + long maxTombstones = Long.MAX_VALUE; + Map failures = new HashMap<>(); + + TombstoneAbortException original = new TombstoneAbortException( + "Max values test", maxNodes, maxTombstones, dataPresent, cl, received, blockFor, failures); + + TombstoneAbortException deserialized = roundTrip(original); + + assertEquals(TombstoneAbortException.class, deserialized.getClass()); + assertEquals(maxNodes, deserialized.nodes); + assertEquals(maxTombstones, deserialized.tombstones); + + // Test with zero/negative values + TombstoneAbortException zeroValues = new TombstoneAbortException( + "Zero values test", 0, 0L, dataPresent, cl, received, blockFor, failures); + + TombstoneAbortException deserializedZero = roundTrip(zeroValues); + + assertEquals(TombstoneAbortException.class, deserializedZero.getClass()); + assertEquals(0, deserializedZero.nodes); + assertEquals(0L, deserializedZero.tombstones); + } + + @Test + public void testFunctionExecutionExceptionWithNullKeyspace() throws IOException + { + FunctionName functionNameNull = new FunctionName(null, "system_function"); + List argTypes = Arrays.asList("text", "int"); + String detail = "System function error"; + + FunctionExecutionException original = new FunctionExecutionException(functionNameNull, argTypes, detail); + FunctionExecutionException deserialized = roundTrip(original); + + assertEquals(FunctionExecutionException.class, deserialized.getClass()); + assertNull(deserialized.functionName.keyspace); + assertEquals("system_function", deserialized.functionName.name); + assertEquals(argTypes, deserialized.argTypes); + assertEquals(detail, deserialized.detail); + } + + @Test + public void testFunctionExecutionExceptionWithEmptyArgTypes() throws IOException + { + FunctionName functionName = new FunctionName("test_ks", "no_arg_function"); + List emptyArgTypes = Arrays.asList(); + String detail = "No arguments function"; + + FunctionExecutionException original = new FunctionExecutionException(functionName, emptyArgTypes, detail); + FunctionExecutionException deserialized = roundTrip(original); + + assertEquals(FunctionExecutionException.class, deserialized.getClass()); + assertEquals("test_ks", deserialized.functionName.keyspace); + assertEquals("no_arg_function", deserialized.functionName.name); + assertEquals(0, deserialized.argTypes.size()); + assertEquals(detail, deserialized.detail); + } + + @Test + public void testPreparedQueryNotFoundExceptionWithZeroId() throws IOException + { + byte[] zeroBytes = new byte[16]; + // All zeros + MD5Digest zeroId = MD5Digest.wrap(zeroBytes); + + PreparedQueryNotFoundException original = new PreparedQueryNotFoundException(zeroId); + PreparedQueryNotFoundException deserialized = roundTrip(original); + + assertArrayEquals(zeroId.bytes, deserialized.id.bytes); + assertNotNull(deserialized.getMessage()); + assertTrue(deserialized.getMessage().contains(zeroId.toString())); + } + + @Test + public void testAlreadyExistsExceptionWithEmptyNames() throws IOException + { + // Test with empty keyspace name + AlreadyExistsException emptyKs = new AlreadyExistsException("", "table"); + AlreadyExistsException deserializedEmptyKs = roundTrip(emptyKs); + + assertEquals(AlreadyExistsException.class, deserializedEmptyKs.getClass()); + assertEquals("", deserializedEmptyKs.ksName); + assertEquals("table", deserializedEmptyKs.cfName); + + // Test with empty table name + AlreadyExistsException emptyTable = new AlreadyExistsException("keyspace", ""); + AlreadyExistsException deserializedEmptyTable = roundTrip(emptyTable); + + assertEquals(AlreadyExistsException.class, deserializedEmptyTable.getClass()); + assertEquals("keyspace", deserializedEmptyTable.ksName); + assertEquals("", deserializedEmptyTable.cfName); + } + + @Test + public void testCasWriteUnknownResultExceptionEdgeCases() throws IOException + { + // Test with zero values + CasWriteUnknownResultException zeroValues = new CasWriteUnknownResultException( + ConsistencyLevel.SERIAL, 0, 0); + CasWriteUnknownResultException deserializedZero = roundTrip(zeroValues); + + assertEquals(CasWriteUnknownResultException.class, deserializedZero.getClass()); + assertEquals(ConsistencyLevel.SERIAL, deserializedZero.consistency); + assertEquals(0, deserializedZero.received); + assertEquals(0, deserializedZero.blockFor); + + // Test with maximum values + CasWriteUnknownResultException maxValues = new CasWriteUnknownResultException( + ConsistencyLevel.LOCAL_SERIAL, Integer.MAX_VALUE, Integer.MAX_VALUE); + CasWriteUnknownResultException deserializedMax = roundTrip(maxValues); + + assertEquals(CasWriteUnknownResultException.class, deserializedMax.getClass()); + assertEquals(ConsistencyLevel.LOCAL_SERIAL, deserializedMax.consistency); + assertEquals(Integer.MAX_VALUE, deserializedMax.received); + assertEquals(Integer.MAX_VALUE, deserializedMax.blockFor); + } + + @Test + public void testDeepNestedCauseChain() throws IOException + { + // Create a deep cause chain + InvalidRequestException root = new InvalidRequestException("Root exception"); + + Throwable current = root; + for (int i = 1; i <= 10; i++) + { + RuntimeException cause = new RuntimeException("Cause level " + i); + current.initCause(cause); + current = cause; + } + + InvalidRequestException deserialized = roundTrip(root); + + assertEquals(InvalidRequestException.class, deserialized.getClass()); + assertEquals("Root exception", deserialized.getMessage()); + + // Verify the deep cause chain is preserved (now using RemoteException format) + Throwable currentDeserialized = deserialized.getCause(); + for (int i = 1; i <= 10; i++) + { + assertNotNull("Cause level " + i + " should not be null", currentDeserialized); + assertTrue("Cause level " + i + " should contain expected message", + currentDeserialized.getMessage().contains("Cause level " + i)); + currentDeserialized = currentDeserialized.getCause(); + } + assertNull("Should not have more causes", currentDeserialized); + } + + @Test + public void testManySuppressedExceptions() throws IOException + { + InvalidRequestException original = new InvalidRequestException("Root with many suppressed"); + + // Add many suppressed exceptions + for (int i = 0; i < 50; i++) + { + original.addSuppressed(new IllegalArgumentException("Suppressed " + i)); + } + + InvalidRequestException deserialized = roundTrip(original); + + assertEquals(InvalidRequestException.class, deserialized.getClass()); + assertEquals("Root with many suppressed", deserialized.getMessage()); + assertEquals(50, deserialized.getSuppressed().length); + + for (int i = 0; i < 50; i++) + { + assertTrue("Suppressed " + i + " should contain expected message", + deserialized.getSuppressed()[i].getMessage().contains("Suppressed " + i)); + } + } + + @Test + public void testCasWriteUnknownResultException() throws IOException + { + ConsistencyLevel cl = ConsistencyLevel.SERIAL; + int received = 2; + int blockFor = 3; + + CasWriteUnknownResultException original = new CasWriteUnknownResultException(cl, received, blockFor); + + CasWriteUnknownResultException deserialized = roundTrip(original); + + // Verify type fidelity + assertEquals(CasWriteUnknownResultException.class, deserialized.getClass()); + + // Verify specific fields + assertEquals(cl, deserialized.consistency); + assertEquals(received, deserialized.received); + assertEquals(blockFor, deserialized.blockFor); + assertNotNull(deserialized.getMessage()); + } + + @Test + public void testPreparedQueryNotFoundException() throws IOException + { + byte[] idBytes = {0x01, 0x23, 0x45, 0x67, (byte)0x89, (byte)0xAB, (byte)0xCD, (byte)0xEF, + 0x01, 0x23, 0x45, 0x67, (byte)0x89, (byte)0xAB, (byte)0xCD, (byte)0xEF}; + MD5Digest id = MD5Digest.wrap(idBytes); + + PreparedQueryNotFoundException original = new PreparedQueryNotFoundException(id); + + PreparedQueryNotFoundException deserialized = roundTrip(original); + + // Verify type fidelity + assertEquals(PreparedQueryNotFoundException.class, deserialized.getClass()); + + // Verify specific fields + assertArrayEquals(id.bytes, deserialized.id.bytes); + assertNotNull(deserialized.getMessage()); + assertTrue(deserialized.getMessage().contains(id.toString())); + } + + @Test + public void testAlreadyExistsException() throws IOException + { + String ksName = "test_keyspace"; + String cfName = "test_table"; + + AlreadyExistsException original = new AlreadyExistsException(ksName, cfName); + + AlreadyExistsException deserialized = roundTrip(original); + + // Verify type fidelity + assertEquals(AlreadyExistsException.class, deserialized.getClass()); + + // Verify specific fields + assertEquals(ksName, deserialized.ksName); + assertEquals(cfName, deserialized.cfName); + assertNotNull(deserialized.getMessage()); + assertTrue(deserialized.getMessage().contains(ksName)); + assertTrue(deserialized.getMessage().contains(cfName)); + } + + @Test + public void testSerializedSizeAccuracy() throws IOException + { + // Test that serialized size calculations are accurate for various exception types + + // Simple exception + InvalidRequestException simple = new InvalidRequestException("Simple message"); + verifySerializedSizeAccuracy(simple); + + // Exception with cause and suppressed + InvalidRequestException complex = new InvalidRequestException("Complex message"); + complex.initCause(new RuntimeException("Cause")); + complex.addSuppressed(new IllegalStateException("Suppressed")); + verifySerializedSizeAccuracy(complex); + + // Exception with custom stack trace + InvalidRequestException withStack = new InvalidRequestException("With stack"); + withStack.setStackTrace(new StackTraceElement[] { + new StackTraceElement("Class1", "method1", "File1.java", 100), + new StackTraceElement("Class2", "method2", null, -1) + }); + verifySerializedSizeAccuracy(withStack); + + // Complex exception with fields + UnavailableException unavailable = UnavailableException.create(ConsistencyLevel.QUORUM, 5, 2); + verifySerializedSizeAccuracy(unavailable); + + // Exception with large data + Map largeFailures = new HashMap<>(); + for (int i = 1; i <= 100; i++) + { + largeFailures.put(InetAddressAndPort.getByName("192.168.1." + i), RequestFailureReason.TIMEOUT); + } + WriteFailureException largeFailure = new WriteFailureException( + ConsistencyLevel.ALL, 50, 100, WriteType.BATCH, largeFailures); + verifySerializedSizeAccuracy(largeFailure); + } + + private void verifySerializedSizeAccuracy(CassandraException exception) throws IOException + { + DataOutputBuffer out = new DataOutputBuffer(); + CassandraException.serializer.serialize(exception, out, VERSION); + + long calculatedSize = CassandraException.serializer.serializedSize(exception, VERSION); + long actualSize = out.getLength(); + + assertEquals("Serialized size calculation mismatch for " + exception.getClass().getSimpleName(), + calculatedSize, actualSize); + } +} diff --git a/test/unit/org/apache/cassandra/service/paxos/CasForwardingTest.java b/test/unit/org/apache/cassandra/service/paxos/CasForwardingTest.java new file mode 100644 index 0000000000..1e43a7c599 --- /dev/null +++ b/test/unit/org/apache/cassandra/service/paxos/CasForwardingTest.java @@ -0,0 +1,196 @@ +/* + * 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.paxos; + +import java.util.Arrays; +import java.util.List; + +import org.junit.BeforeClass; +import org.junit.Test; +import static org.junit.Assert.*; + +import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.statements.CQL3CasRequest; +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.RegularAndStaticColumns; +import org.apache.cassandra.db.rows.RowIterator; +import org.apache.cassandra.exceptions.UnavailableException; +import org.apache.cassandra.io.util.DataInputBuffer; +import org.apache.cassandra.io.util.DataOutputBuffer; +import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.utils.ByteBufferUtil; + +public class CasForwardingTest +{ + private static final String KEYSPACE1 = "CasForwardingTest"; + private static final String CF_STANDARD1 = "Standard1"; + + @BeforeClass + public static void defineSchema() + { + DatabaseDescriptor.daemonInitialization(); + SchemaLoader.prepareServer(); + SchemaLoader.createKeyspace(KEYSPACE1, + KeyspaceParams.simple(1), + SchemaLoader.standardCFMD(KEYSPACE1, CF_STANDARD1)); + } + + @Test + public void testCasForwardResponseExceptionHandling() throws Exception + { + // Test exception forwarding in CasForwardResponse + UnavailableException testException = new UnavailableException("Test exception", ConsistencyLevel.QUORUM, 3, 1); + + CasForwardResponse response = new CasForwardResponse(testException, null); + + assertFalse("Response should not be successful", response.isSuccess()); + assertEquals("Exception should match", testException, response.exception); + assertNull("Result should be null when exception is present", response.result); + assertTrue("Warnings should be empty", response.warnings.isEmpty()); + } + + @Test + public void testCasForwardRequestWithRemoteClientState() throws Exception + { + // Test CasForwardRequest with RemoteClientState serialization + TableMetadata metadata = Schema.instance.getTableMetadata(KEYSPACE1, CF_STANDARD1); + DecoratedKey key = DatabaseDescriptor.getPartitioner().decorateKey(ByteBufferUtil.bytes("test")); + ClientState localState = ClientState.forInternalCalls(); + localState.setKeyspace(KEYSPACE1); + + // Create a real CQL3CasRequest + CQL3CasRequest casRequest = new CQL3CasRequest(metadata, key, RegularAndStaticColumns.NONE, true, false); + + CasForwardRequest request = new CasForwardRequest( + KEYSPACE1, + CF_STANDARD1, + key, + ConsistencyLevel.QUORUM, + ConsistencyLevel.QUORUM, + System.currentTimeMillis(), + localState, + casRequest + ); + + // Verify RemoteClientState was created correctly + assertNotNull("Client state should not be null", request.clientState); + + // Verify other fields + assertEquals("Keyspace name should match", KEYSPACE1, request.keyspaceName); + assertEquals("CF name should match", CF_STANDARD1, request.cfName); + assertEquals("Consistency for paxos should match", ConsistencyLevel.QUORUM, request.consistencyForPaxos); + assertEquals("Consistency for commit should match", ConsistencyLevel.QUORUM, request.consistencyForCommit); + assertNotNull("CAS request should not be null", request.casRequest); + } + + @Test + public void testCasForwardResponseSerialization() throws Exception + { + // Test exception serialization + UnavailableException testException = new UnavailableException("Test serialization exception", ConsistencyLevel.QUORUM, 3, 1); + CasForwardResponse originalResponse = new CasForwardResponse(testException, null); + + // Serialize + DataOutputBuffer out = new DataOutputBuffer(); + CasForwardResponse.serializer.serialize(originalResponse, out, 0); + + // Deserialize + DataInputBuffer in = new DataInputBuffer(out.toByteArray()); + CasForwardResponse deserializedResponse = CasForwardResponse.serializer.deserialize(in, 0); + + // Verify exception is preserved + assertFalse("Response should not be successful", deserializedResponse.isSuccess()); + assertNotNull("Exception should not be null", deserializedResponse.exception); + assertEquals("Exception type should match", testException.getClass(), deserializedResponse.exception.getClass()); + + // Note: UnavailableException reconstructs its message from consistency level, required, and alive values, + // so we verify the exception type and key properties rather than the exact message + UnavailableException deserializedException = (UnavailableException) deserializedResponse.exception; + assertEquals("Consistency level should match", testException.consistency, deserializedException.consistency); + assertEquals("Required replicas should match", testException.required, deserializedException.required); + assertEquals("Alive replicas should match", testException.alive, deserializedException.alive); + } + + @Test + public void testCasForwardResponseSerializedSizeAccuracy() throws Exception + { + // Test that serializedSize method returns accurate sizes + UnavailableException testException = new UnavailableException("Size test exception", ConsistencyLevel.QUORUM, 3, 1); + CasForwardResponse response = new CasForwardResponse(testException, null); + + // Calculate expected size + long calculatedSize = CasForwardResponse.serializer.serializedSize(response, 0); + + // Serialize and measure actual size + DataOutputBuffer out = new DataOutputBuffer(); + CasForwardResponse.serializer.serialize(response, out, 0); + + long actualSize = out.getLength(); + + assertEquals("Calculated size should match actual serialized size", calculatedSize, actualSize); + } + + @Test + public void testCasForwardResponseWarningsSerialization() throws Exception + { + // Test warnings serialization in CasForwardResponse + List warnings = Arrays.asList("Warning 1", "Warning 2", "Test warning message"); + CasForwardResponse originalResponse = new CasForwardResponse((RowIterator) null, warnings); + + // Serialize + DataOutputBuffer out = new DataOutputBuffer(); + CasForwardResponse.serializer.serialize(originalResponse, out, 0); + + // Deserialize + DataInputBuffer in = new DataInputBuffer(out.toByteArray()); + CasForwardResponse deserializedResponse = CasForwardResponse.serializer.deserialize(in, 0); + + // Verify warnings are preserved + assertTrue("Response should be successful", deserializedResponse.isSuccess()); + assertFalse("Warnings should not be empty", deserializedResponse.warnings.isEmpty()); + assertEquals("Warnings should match", warnings, deserializedResponse.warnings); + } + + @Test + public void testConsensusReadForwardResponseWarningsSerialization() throws Exception + { + // Test warnings serialization in CasForwardResponse (with PartitionIterator constructor) + List warnings = Arrays.asList("Read warning 1", "Read warning 2"); + CasForwardResponse originalResponse = new CasForwardResponse( + (org.apache.cassandra.db.partitions.PartitionIterator) null, warnings); + + // Serialize + DataOutputBuffer out = new DataOutputBuffer(); + CasForwardResponse.serializer.serialize(originalResponse, out, 0); + + // Deserialize + DataInputBuffer in = new DataInputBuffer(out.toByteArray()); + CasForwardResponse deserializedResponse = CasForwardResponse.serializer.deserialize(in, 0); + + // Verify warnings are preserved + assertTrue("Response should be successful", deserializedResponse.isSuccess()); + assertNotNull("Warnings should not be null", deserializedResponse.warnings); + assertEquals("Warning count should match", warnings.size(), deserializedResponse.warnings.size()); + assertEquals("Warnings should match", warnings, deserializedResponse.warnings); + } +} \ No newline at end of file diff --git a/test/unit/org/apache/cassandra/service/paxos/ConsensusReadForwardingTest.java b/test/unit/org/apache/cassandra/service/paxos/ConsensusReadForwardingTest.java new file mode 100644 index 0000000000..3e1a80cf8c --- /dev/null +++ b/test/unit/org/apache/cassandra/service/paxos/ConsensusReadForwardingTest.java @@ -0,0 +1,56 @@ +/* + * 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.paxos; + +import org.junit.BeforeClass; +import org.junit.Test; +import static org.junit.Assert.*; + +import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.exceptions.UnavailableException; +import org.apache.cassandra.schema.KeyspaceParams; + +public class ConsensusReadForwardingTest +{ + private static final String KEYSPACE1 = "ConsensusReadForwardingTest"; + private static final String CF_STANDARD1 = "Standard1"; + + @BeforeClass + public static void defineSchema() + { + DatabaseDescriptor.daemonInitialization(); + SchemaLoader.prepareServer(); + SchemaLoader.createKeyspace(KEYSPACE1, + KeyspaceParams.simple(1), + SchemaLoader.standardCFMD(KEYSPACE1, CF_STANDARD1)); + } + + @Test + public void testConsensusReadForwardResponseWithException() + { + // Test exception handling in CasForwardResponse + UnavailableException testException = new UnavailableException("Test consensus read exception", ConsistencyLevel.QUORUM, 3, 1); + CasForwardResponse response = new CasForwardResponse(testException, null); + + assertFalse("Response should not be successful", response.isSuccess()); + assertEquals("Exception should match", testException, response.exception); + assertNull("Result should be null when exception is present", response.partitionIterator()); + } +} \ No newline at end of file diff --git a/test/unit/org/apache/cassandra/service/paxos/PaxosStateTest.java b/test/unit/org/apache/cassandra/service/paxos/PaxosStateTest.java index 28f0ecc44e..e080e4fc11 100644 --- a/test/unit/org/apache/cassandra/service/paxos/PaxosStateTest.java +++ b/test/unit/org/apache/cassandra/service/paxos/PaxosStateTest.java @@ -293,7 +293,7 @@ public class PaxosStateTest private static void assertAcceptedMerge(Accepted expected, Accepted left, Accepted right) { - Committed empty = Committed.none(expected.update.partitionKey(), expected.update.metadata()); + Committed empty = Committed.none(expected.partitionKey(), expected.metadata()); Snapshot snapshotLeft = new Snapshot(null, null, left, empty); Snapshot snapshotRight = new Snapshot(null, null, right, empty); Accepted merged = Snapshot.merge(snapshotLeft, snapshotRight).accepted; diff --git a/test/unit/org/apache/cassandra/service/paxos/uncommitted/PaxosRowsTest.java b/test/unit/org/apache/cassandra/service/paxos/uncommitted/PaxosRowsTest.java index f410424499..d1ca75637f 100644 --- a/test/unit/org/apache/cassandra/service/paxos/uncommitted/PaxosRowsTest.java +++ b/test/unit/org/apache/cassandra/service/paxos/uncommitted/PaxosRowsTest.java @@ -74,12 +74,12 @@ public class PaxosRowsTest static Commit emptyCommitFor(Ballot ballot, DecoratedKey key) { - return new Commit(ballot, PartitionUpdate.emptyUpdate(metadata, key)); + return Commit.create(ballot, PartitionUpdate.emptyUpdate(metadata, key)); } static Commit nonEmptyCommitFor(Ballot ballot, DecoratedKey key) { - return new Commit(ballot, nonEmptyUpdate(ballot, metadata, key)); + return Commit.create(ballot, nonEmptyUpdate(ballot, metadata, key)); } static PartitionUpdate nonEmptyUpdate(Ballot ballot, TableMetadata cfm, DecoratedKey key) diff --git a/test/unit/org/apache/cassandra/service/paxos/uncommitted/PaxosStateTrackerTest.java b/test/unit/org/apache/cassandra/service/paxos/uncommitted/PaxosStateTrackerTest.java index 9836f40594..a1f806bb05 100644 --- a/test/unit/org/apache/cassandra/service/paxos/uncommitted/PaxosStateTrackerTest.java +++ b/test/unit/org/apache/cassandra/service/paxos/uncommitted/PaxosStateTrackerTest.java @@ -115,7 +115,7 @@ public class PaxosStateTrackerTest private static Commit commit(TableMetadata cfm, int k, Ballot ballot) { - return new Commit(ballot, update(cfm, k, ballot)); + return Commit.create(ballot, update(cfm, k, ballot)); } private static void savePaxosRepair(TableMetadata cfm, Range range, Ballot lowBound)