You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by bd...@apache.org on 2026/04/14 21:46:51 UTC
(cassandra) 02/36: Mutation tracking journal integration, read, and write path
This is an automated email from the ASF dual-hosted git repository.
bdeggleston pushed a commit to branch cep-45-mutation-tracking
in repository https://gitbox.apache.org/repos/asf/cassandra.git
commit c71f97695503b87d3b70ede9db3b0872389cc448
Author: Blake Eggleston <bl...@ultrablake.com>
AuthorDate: Thu May 15 16:18:41 2025 -0700
Mutation tracking journal integration, read, and write path
Patch by Blake Eggleston & Aleksey Yeshchenko
Reviewed by Blake Eggleston, Aleksey Yeshchenko, & Abe Ratnofsky
for CASSANDRA-20304, CASSANDRA-20305, & CASSANDRA-20308, CASSANDRA-20373
Co-authored-by: Blake Eggleston <bl...@ultrablake.com>
Co-authored-by: Aleksey Yeschenko <al...@apache.org>
---
CHANGES.txt | 9 +-
pylib/cqlshlib/test/test_cqlsh_output.py | 2 +-
src/java/org/apache/cassandra/config/Config.java | 2 +
.../cassandra/config/DatabaseDescriptor.java | 14 +
.../cassandra/cql3/statements/BatchStatement.java | 2 +-
.../cql3/statements/BatchUpdatesCollector.java | 3 +-
.../cql3/statements/ModificationStatement.java | 32 +-
.../cql3/statements/schema/KeyspaceAttributes.java | 13 +-
...er.java => AbstractReadCommandVerbHandler.java} | 117 +-
.../org/apache/cassandra/db/CounterMutation.java | 21 +-
src/java/org/apache/cassandra/db/IMutation.java | 3 +
src/java/org/apache/cassandra/db/Keyspace.java | 58 +-
src/java/org/apache/cassandra/db/Mutation.java | 80 +-
.../cassandra/db/PartitionRangeReadCommand.java | 25 +
src/java/org/apache/cassandra/db/ReadCommand.java | 172 ++-
.../cassandra/db/ReadCommandVerbHandler.java | 156 +-
.../cassandra/db/ReadExecutionController.java | 2 +
.../org/apache/cassandra/db/SimpleBuilders.java | 9 +-
.../cassandra/db/SinglePartitionReadCommand.java | 20 +
.../db/commitlog/CommitLogDescriptor.java | 5 +-
.../cassandra/db/commitlog/CommitLogReplayer.java | 2 +-
.../db/partitions/AbstractPartitionIterator.java | 26 +
.../db/partitions/PartitionIterators.java | 43 +
.../db/partitions/SimpleBTreePartition.java | 131 ++
.../cassandra/db/rows/AbstractRowIterator.java | 72 +
.../cassandra/db/rows/ComplexColumnData.java | 2 +-
.../org/apache/cassandra/db/rows/RowIterators.java | 281 +++-
.../cassandra/db/rows/UnfilteredSerializer.java | 8 +-
.../db/streaming/CassandraStreamReceiver.java | 3 +-
.../db/tracked/TrackedKeyspaceWriteHandler.java | 82 ++
.../org/apache/cassandra/db/view/TableViews.java | 5 +-
.../cassandra/db/virtual/VirtualMutation.java | 13 +
.../apache/cassandra/hints/HintsDescriptor.java | 5 +-
src/java/org/apache/cassandra/journal/Journal.java | 7 +-
.../cassandra/metrics/ReadRepairMetrics.java | 1 +
.../org/apache/cassandra/metrics/TableMetrics.java | 3 +-
src/java/org/apache/cassandra/net/Message.java | 25 +-
.../org/apache/cassandra/net/MessagingService.java | 9 +-
src/java/org/apache/cassandra/net/Verb.java | 18 +
.../cassandra/replication/CoordinatorLog.java | 240 ++++
.../cassandra/replication/CoordinatorLogId.java | 127 ++
.../cassandra/replication/LocalMutationStates.java | 187 +++
.../cassandra/replication/Log2OffsetsMap.java | 230 +++
.../apache/cassandra/replication/MutationId.java | 135 ++
.../tracking => replication}/MutationJournal.java | 114 +-
.../cassandra/replication/MutationSummary.java | 351 +++++
.../replication/MutationTrackingService.java | 239 ++++
.../org/apache/cassandra/replication/Offsets.java | 1496 ++++++++++++++++++++
.../apache/cassandra/replication/Participants.java | 55 +
.../cassandra/replication/ReconciliationPlan.java | 210 +++
.../org/apache/cassandra/replication/Shard.java | 117 ++
.../cassandra/replication/ShortMutationId.java | 117 ++
.../cassandra/replication/TrackedWriteRequest.java | 287 ++++
.../schema/DistributedMetadataLogKeyspace.java | 2 +-
.../apache/cassandra/schema/KeyspaceMetadata.java | 19 +-
.../apache/cassandra/schema/KeyspaceParams.java | 71 +-
.../apache/cassandra/schema/ReplicationType.java | 92 ++
.../apache/cassandra/schema/SchemaKeyspace.java | 25 +-
.../org/apache/cassandra/schema/TableMetadata.java | 24 +
src/java/org/apache/cassandra/schema/Tables.java | 7 +
.../apache/cassandra/service/CassandraDaemon.java | 2 +
.../org/apache/cassandra/service/StorageProxy.java | 145 +-
.../apache/cassandra/service/StorageService.java | 4 +
.../service/TrackedWriteResponseHandler.java | 117 ++
.../org/apache/cassandra/service/paxos/Commit.java | 11 +-
.../service/reads/AbstractReadExecutor.java | 1 +
.../service/reads/range/RangeCommandIterator.java | 58 +-
.../service/reads/range/SingleRangeResponse.java | 2 +-
.../service/reads/repair/BlockingReadRepairs.java | 3 +-
.../reads/tracked/AbstractPartialTrackedRead.java | 154 ++
.../reads/tracked/PartialTrackedRangeRead.java | 347 +++++
.../service/reads/tracked/PartialTrackedRead.java | 88 ++
.../tracked/PartialTrackedSinglePartitionRead.java | 108 ++
.../service/reads/tracked/ReadReconcileNotify.java | 95 ++
.../reads/tracked/ReadReconcileReceive.java | 138 ++
.../service/reads/tracked/ReadReconcileSend.java | 166 +++
.../service/reads/tracked/TrackedDataResponse.java | 99 ++
.../reads/tracked/TrackedLocalReadCoordinator.java | 617 ++++++++
.../service/reads/tracked/TrackedLocalReads.java | 182 +++
.../service/reads/tracked/TrackedRead.java | 520 +++++++
.../reads/tracked/TrackedSummaryResponse.java | 77 +
.../cassandra/service/tracking/MutationId.java | 84 --
src/java/org/apache/cassandra/tcm/Startup.java | 10 +-
.../cassandra/tcm/membership/NodeVersion.java | 2 +-
.../tcm/sequences/CancelCMSReconfiguration.java | 2 +-
.../cassandra/tcm/serialization/Version.java | 5 +
.../cms/PrepareCMSReconfiguration.java | 2 +-
.../cassandra/distributed/impl/Instance.java | 5 +
.../cassandra/distributed/test/GroupByTest.java | 521 ++++---
.../ReadRepairEmptyRangeTombstonesTestBase.java | 17 +-
.../distributed/test/ReadRepairInQueriesTest.java | 10 +
.../distributed/test/ReadRepairQueryTester.java | 80 +-
.../test/ReadRepairRangeQueriesTest.java | 15 +-
.../test/ReadRepairSliceQueriesTest.java | 3 +
...ReadRepairTest.java => ReadRepairTestBase.java} | 227 +--
.../distributed/test/ReadRepairTester.java | 23 +-
.../distributed/test/ReadRepairTrackedTest.java | 30 +
.../distributed/test/ReadRepairUntrackedTest.java | 43 +
.../test/ReplicaFilteringProtectionTest.java | 3 +-
.../distributed/test/ResourceLeakTest.java | 2 +-
.../distributed/test/ShortReadProtectionTest.java | 31 +-
.../tracking/MutationTrackingPendingReadTest.java | 250 ++++
.../MutationTrackingReadReconciliationTest.java | 390 +++++
.../test/tracking/MutationTrackingTest.java | 127 ++
.../test/tracking/MutationTrackingUtils.java | 301 ++++
.../db/commitlog/CommitLogStressTest.java | 2 +
.../cassandra/AbstractSerializationsTester.java | 3 +-
.../cql3/statements/DescribeStatementTest.java | 22 +-
.../apache/cassandra/db/ColumnFamilyStoreTest.java | 3 +-
.../apache/cassandra/db/CounterMutationTest.java | 3 +-
.../apache/cassandra/db/DeletePartitionTest.java | 3 +-
.../db/MutationVerbHandlerOutOfRangeTest.java | 2 +
.../db/ReadCommandVerbHandlerOutOfRangeTest.java | 2 +
.../cassandra/db/ReadCommandVerbHandlerTest.java | 2 +
.../org/apache/cassandra/db/RowUpdateBuilder.java | 3 +-
.../db/compaction/CompactionsPurgeTest.java | 7 +-
.../org/apache/cassandra/dht/BootStrapperTest.java | 2 +
test/unit/org/apache/cassandra/hints/HintTest.java | 3 +-
.../apache/cassandra/hints/HintsCatalogTest.java | 3 +-
.../apache/cassandra/index/sasi/SASIIndexTest.java | 67 +-
.../RepairMessageVerbHandlerOutOfRangeTest.java | 2 +
.../cassandra/replication/CoordinatorLogTest.java | 129 ++
.../MutationJournalTest.java | 10 +-
.../apache/cassandra/replication/OffsetsTest.java | 776 ++++++++++
.../schema/SchemaMetadataSerializationTest.java | 12 +-
.../cassandra/service/JoinTokenRingTest.java | 2 +
.../apache/cassandra/service/PaxosStateTest.java | 2 +
.../cassandra/service/StorageServiceTest.java | 2 +
.../cassandra/service/paxos/PaxosStateTest.java | 2 +
.../paxos/PaxosVerbHandlerOutOfRangeTest.java | 2 +
.../reads/repair/RepairedDataVerifierTest.java | 2 +
.../streaming/StreamSessionOwnedRangesTest.java | 2 +
.../apache/cassandra/tools/OfflineToolUtils.java | 3 +-
.../cassandra/triggers/TriggerExecutorTest.java | 19 +-
.../cassandra/utils/CassandraGenerators.java | 20 +-
135 files changed, 10877 insertions(+), 978 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 83b3bb7d67..4ca05b0360 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,9 @@
+cep-45-mutation-tracking
+ * Fix mutation tracking startup (CASSANDRA-20540)
+ * Mutation tracking journal integration, read, and write path (CASSANDRA-20304, CASSANDRA-20305, CASSANDRA-20308)
+ * Introduce MutationJournal for coordinator logs (CASSANDRA-20353)
+ * Copy over Journal and dependencies from cep-15-accord (CASSANDRA-20321)
+
6.0-alpha2
* Fix a removed TTLed row re-appearance in a materialized view after a cursor compaction (CASSANDRA-21152)
* Rework ZSTD dictionary compression logic to create a trainer per training (CASSANDRA-21209)
@@ -7,7 +13,6 @@ Merged from 4.1:
Merged from 4.0:
* Rate limit password changes (CASSANDRA-21202)
-
6.0-alpha1
* Improve performance when calculating settled placements during range movements (CASSANDRA-21144)
* Make shadow gossip round parameters configurable for testing (CASSANDRA-21149)
@@ -107,6 +112,8 @@ Merged from 4.0:
* Optimize DataPlacement lookup by ReplicationParams (CASSANDRA-20804)
* Fix ShortPaxosSimulationTest and AccordSimulationRunner do not execute from the cli (CASSANDRA-20805)
* Allow overriding arbitrary settings via environment variables (CASSANDRA-20749)
+
+5.1
* Optimize MessagingService.getVersionOrdinal (CASSANDRA-20816)
* Optimize TrieMemtable#getFlushSet (CASSANDRA-20760)
* Support manual secondary index selection at the CQL level (CASSANDRA-18112)
diff --git a/pylib/cqlshlib/test/test_cqlsh_output.py b/pylib/cqlshlib/test/test_cqlsh_output.py
index c32690b424..580e302c7a 100644
--- a/pylib/cqlshlib/test/test_cqlsh_output.py
+++ b/pylib/cqlshlib/test/test_cqlsh_output.py
@@ -794,7 +794,7 @@ class TestCqlshOutput(BaseTestCase):
self.assertNoHasColors(output)
# Since CASSANDRA-7622 'DESC FULL SCHEMA' also shows all VIRTUAL keyspaces
self.assertIn('VIRTUAL KEYSPACE system_virtual_schema', output)
- self.assertIn("\nCREATE KEYSPACE system_auth WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true AND fast_path = 'simple';\n",
+ self.assertIn("\nCREATE KEYSPACE system_auth WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true AND fast_path = 'simple' AND replication_type = 'untracked';\n",
output)
self.assertRegex(output, r'.*\s*$')
diff --git a/src/java/org/apache/cassandra/config/Config.java b/src/java/org/apache/cassandra/config/Config.java
index c777d0fc89..2269cb2eda 100644
--- a/src/java/org/apache/cassandra/config/Config.java
+++ b/src/java/org/apache/cassandra/config/Config.java
@@ -694,6 +694,8 @@ public class Config
public boolean dynamic_data_masking_enabled = false;
+ public boolean mutation_tracking_enabled = false;
+
/**
* Time in milliseconds after a warning will be emitted to the log and to the client that a UDF runs too long.
* (Only valid, if user_defined_functions_threads_enabled==true)
diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
index 59ffb9f02a..d7c997ba1e 100644
--- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
+++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
@@ -6025,6 +6025,20 @@ public class DatabaseDescriptor
}
}
+ public static boolean getMutationTrackingEnabled()
+ {
+ return conf.mutation_tracking_enabled;
+ }
+
+ public static void setMutationTrackingEnabled(boolean enabled)
+ {
+ if (enabled != conf.mutation_tracking_enabled)
+ {
+ logger.info("Setting mutation_tracking_enabled to {}", enabled);
+ conf.mutation_tracking_enabled = enabled;
+ }
+ }
+
public static OptionalDouble getSeverityDuringDecommission()
{
return conf.severity_during_decommission > 0 ?
diff --git a/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java b/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java
index 8a38387d88..6fd03204ce 100644
--- a/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java
@@ -542,7 +542,7 @@ public class BatchStatement implements CQLStatement.CompositeCQLStatement
updatePartitionsPerBatchMetrics(mutations.size());
boolean mutateAtomic = (isLogged() && mutations.size() > 1);
- StorageProxy.mutateWithTriggers(mutations, cl, mutateAtomic, requestTime, preserveTimestamp);
+ StorageProxy.mutateWithoutConditions(mutations, cl, mutateAtomic, requestTime, preserveTimestamp);
ClientRequestSizeMetrics.recordRowAndColumnCountMetrics(mutations);
}
diff --git a/src/java/org/apache/cassandra/cql3/statements/BatchUpdatesCollector.java b/src/java/org/apache/cassandra/cql3/statements/BatchUpdatesCollector.java
index 4bc0d909d2..1db782f1c5 100644
--- a/src/java/org/apache/cassandra/cql3/statements/BatchUpdatesCollector.java
+++ b/src/java/org/apache/cassandra/cql3/statements/BatchUpdatesCollector.java
@@ -38,6 +38,7 @@ import org.apache.cassandra.db.commitlog.CommitLogSegment;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.db.virtual.VirtualMutation;
import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.ClientState;
@@ -224,7 +225,7 @@ final class BatchUpdatesCollector implements UpdatesCollector
PartitionUpdate update = updateEntry.getValue().build();
updates.put(updateEntry.getKey(), update);
}
- return new Mutation(keyspaceName, key, updates.build(), createdAt, potentialTxnConflicts);
+ return new Mutation(MutationId.none(), keyspaceName, key, updates.build(), createdAt, potentialTxnConflicts);
}
public PartitionUpdate.Builder get(TableId tableId)
diff --git a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java
index fd853004be..9e98c43c66 100644
--- a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java
@@ -72,9 +72,11 @@ import org.apache.cassandra.cql3.transactions.ReferenceOperation;
import org.apache.cassandra.db.CBuilder;
import org.apache.cassandra.db.Clustering;
import org.apache.cassandra.db.ConsistencyLevel;
+import org.apache.cassandra.db.CounterMutation;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.IMutation;
import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts;
import org.apache.cassandra.db.ReadExecutionController;
import org.apache.cassandra.db.RegularAndStaticColumns;
@@ -107,6 +109,8 @@ import org.apache.cassandra.locator.NetworkTopologyStrategy;
import org.apache.cassandra.locator.Replica;
import org.apache.cassandra.locator.ReplicaLayout;
import org.apache.cassandra.metrics.ClientRequestSizeMetrics;
+import org.apache.cassandra.replication.MutationId;
+import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.SchemaConstants;
@@ -676,11 +680,11 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa
false,
options.getTimestamp(queryState),
options.getNowInSeconds(queryState),
- requestTime
- );
+ requestTime);
+
if (!mutations.isEmpty())
{
- StorageProxy.mutateWithTriggers(mutations, cl, false, requestTime, attrs.isTimestampSet() ? PreserveTimestamp.yes : PreserveTimestamp.no);
+ StorageProxy.mutateWithoutConditions(mutations, cl, false, requestTime, attrs.isTimestampSet() ? PreserveTimestamp.yes : PreserveTimestamp.no);
if (!SchemaConstants.isSystemKeyspace(metadata.keyspace))
ClientRequestSizeMetrics.recordRowAndColumnCountMetrics(mutations);
@@ -842,8 +846,28 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa
{
long timestamp = options.getTimestamp(queryState);
long nowInSeconds = options.getNowInSeconds(queryState);
- for (IMutation mutation : getMutations(queryState.getClientState(), options, true, timestamp, nowInSeconds, requestTime))
+ List<? extends IMutation> mutations = getMutations(queryState.getClientState(), options, true, timestamp, nowInSeconds, requestTime);
+ boolean isTracked = !mutations.isEmpty() && Schema.instance.getKeyspaceMetadata(mutations.get(0).getKeyspaceName()).params.replicationType.isTracked();
+ if (isTracked)
+ {
+ if (mutations.stream().anyMatch(m -> m instanceof CounterMutation))
+ throw new InvalidRequestException("Mutation tracking is currently unsupported with counters");
+ if (mutations.size() > 1)
+ throw new InvalidRequestException("Mutation tracking is currently unsupported with unlogged batches");
+
+ Mutation mutation = (Mutation) mutations.get(0);
+
+ String keyspaceName = mutation.getKeyspaceName();
+ Token token = mutation.key().getToken();
+ MutationId id = MutationTrackingService.instance.nextMutationId(keyspaceName, token);
+ mutation = mutation.withMutationId(id);
mutation.apply();
+ }
+ else
+ {
+ for (IMutation mutation : mutations)
+ mutation.apply();
+ }
return null;
}
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/KeyspaceAttributes.java b/src/java/org/apache/cassandra/cql3/statements/schema/KeyspaceAttributes.java
index d4ca44cdcf..27ee037ab3 100644
--- a/src/java/org/apache/cassandra/cql3/statements/schema/KeyspaceAttributes.java
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/KeyspaceAttributes.java
@@ -29,6 +29,7 @@ import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.SyntaxException;
import org.apache.cassandra.schema.KeyspaceParams;
import org.apache.cassandra.schema.KeyspaceParams.Option;
+import org.apache.cassandra.schema.ReplicationType;
import org.apache.cassandra.schema.ReplicationParams;
import org.apache.cassandra.service.accord.fastpath.FastPathStrategy;
@@ -103,18 +104,26 @@ public final class KeyspaceAttributes extends PropertyDefinitions
{
boolean durableWrites = getBoolean(Option.DURABLE_WRITES.toString(), KeyspaceParams.DEFAULT_DURABLE_WRITES);
FastPathStrategy fastPath = getFastPathStrategy();
- return KeyspaceParams.create(durableWrites, getAllReplicationOptions(), fastPath != null ? fastPath : FastPathStrategy.simple());
+
+ String rtypeName = getString(Option.REPLICATION_TYPE.toString());
+ ReplicationType replicationType = rtypeName != null ? ReplicationType.valueOf(rtypeName) : KeyspaceParams.DEFAULT_REPLICATION_TYPE;
+
+ return KeyspaceParams.create(durableWrites, getAllReplicationOptions(), fastPath != null ? fastPath : FastPathStrategy.simple(), replicationType);
}
KeyspaceParams asAlteredKeyspaceParams(KeyspaceParams previous)
{
boolean durableWrites = getBoolean(Option.DURABLE_WRITES.toString(), previous.durableWrites);
+ String rtypeName = getString(Option.REPLICATION_TYPE.toString());
+ ReplicationType replicationType = rtypeName != null ? ReplicationType.valueOf(rtypeName) : previous.replicationType;
+
+
Map<String, String> previousOptions = previous.replication.options;
ReplicationParams replication = getReplicationStrategyClass() == null
? previous.replication
: ReplicationParams.fromMapWithDefaults(getAllReplicationOptions(), previousOptions);
FastPathStrategy fastPath = getFastPathStrategy();
- return new KeyspaceParams(durableWrites, replication, fastPath != null ? fastPath : previous.fastPath);
+ return new KeyspaceParams(durableWrites, replication, fastPath != null ? fastPath : previous.fastPath, replicationType);
}
public boolean hasOption(Option option)
diff --git a/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java b/src/java/org/apache/cassandra/db/AbstractReadCommandVerbHandler.java
similarity index 63%
copy from src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java
copy to src/java/org/apache/cassandra/db/AbstractReadCommandVerbHandler.java
index 8ff89f4e9b..9647891f8a 100644
--- a/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java
+++ b/src/java/org/apache/cassandra/db/AbstractReadCommandVerbHandler.java
@@ -15,20 +15,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.cassandra.db;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+package org.apache.cassandra.db;
import org.apache.cassandra.config.DatabaseDescriptor;
-import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
import org.apache.cassandra.dht.AbstractBounds;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.exceptions.CoordinatorBehindException;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.InvalidRoutingException;
-import org.apache.cassandra.exceptions.QueryCancelledException;
-import org.apache.cassandra.exceptions.RetryOnDifferentSystemException;
import org.apache.cassandra.locator.Replica;
import org.apache.cassandra.metrics.TCMMetrics;
import org.apache.cassandra.net.IVerbHandler;
@@ -39,35 +34,21 @@ import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.ClusterMetadataService;
import org.apache.cassandra.tcm.Epoch;
-import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.utils.FBUtilities;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
-import static org.apache.cassandra.exceptions.RequestFailureReason.RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM;
-public class ReadCommandVerbHandler implements IVerbHandler<ReadCommand>
+public abstract class AbstractReadCommandVerbHandler<T> implements IVerbHandler<T>
{
- public static final ReadCommandVerbHandler instance = new ReadCommandVerbHandler();
-
- private static final Logger logger = LoggerFactory.getLogger(ReadCommandVerbHandler.class);
-
- public ReadResponse doRead(ReadCommand command, boolean trackRepairedData)
- {
- ReadResponse response;
- try (ReadExecutionController controller = command.executionController(trackRepairedData);
- UnfilteredPartitionIterator iterator = command.executeLocally(controller))
- {
- response = command.createResponse(iterator, controller.getRepairedDataInfo());
- }
+ protected abstract void performRead(Message<T> message, ClusterMetadata metadata);
- return response;
- }
+ protected abstract ReadCommand getCommand(T payload);
- public void doVerb(Message<ReadCommand> message)
+ public final void doVerb(Message<T> message)
{
+ ClusterMetadata metadata = ClusterMetadata.current();
if (message.epoch().isAfter(Epoch.EMPTY))
{
- ClusterMetadata metadata = ClusterMetadata.current();
metadata = checkTokenOwnership(metadata, message);
metadata = checkSchemaVersion(metadata, message);
}
@@ -75,70 +56,21 @@ public class ReadCommandVerbHandler implements IVerbHandler<ReadCommand>
MessageParams.reset();
long timeout = message.expiresAtNanos() - message.createdAtNanos();
- ReadCommand command = message.payload;
+ ReadCommand command = getCommand(message.payload);
command.setMonitoringTime(message.createdAtNanos(), message.isCrossNode(), timeout, DatabaseDescriptor.getSlowQueryTimeout(NANOSECONDS));
if (message.trackWarnings())
command.trackWarnings();
- ReadResponse response;
- try
- {
- response = doRead(command, message.trackRepairedData());
- }
- catch (RejectException e)
- {
- if (!command.isTrackingWarnings())
- throw e;
-
- // make sure to log as the exception is swallowed
- logger.error(e.getMessage());
-
- response = command.createEmptyResponse();
- Message<ReadResponse> reply = message.responseWith(response);
- reply = MessageParams.addToMessage(reply);
-
- MessagingService.instance().send(reply, message.from());
- return;
- }
- catch (RetryOnDifferentSystemException e)
- {
- logger.debug("Responding with retry on different system");
- MessagingService.instance().respondWithFailure(RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM, message);
- Tracing.trace("Payload application resulted in RetryOnDifferentSysten");
- return;
- }
- catch (AssertionError t)
- {
- throw new AssertionError(String.format("Caught an error while trying to process the command: %s", command.toCQLString()), t);
- }
- catch (QueryCancelledException e)
- {
- logger.debug("Query cancelled (timeout)", e);
- response = null;
- assert !command.isCompleted() : "Read marked as completed despite being aborted by timeout to table " + command.metadata();
- }
-
- if (command.complete())
- {
- Tracing.trace("Enqueuing response to {}", message.from());
- Message<ReadResponse> reply = message.responseWith(response);
- reply = MessageParams.addToMessage(reply);
- MessagingService.instance().send(reply, message.from());
- }
- else
- {
- Tracing.trace("Discarding partial response to {} (timed out)", message.from());
- MessagingService.instance().metrics.recordDroppedMessage(message, message.elapsedSinceCreated(NANOSECONDS), NANOSECONDS);
- }
+ performRead(message, metadata);
}
- private ClusterMetadata checkSchemaVersion(ClusterMetadata metadata, Message<ReadCommand> message)
+ private ClusterMetadata checkSchemaVersion(ClusterMetadata metadata, Message<T> message)
{
- ReadCommand readCommand = message.payload;
+ ReadCommand readCommand = getCommand(message.payload);
if (SchemaConstants.isSystemKeyspace(readCommand.metadata().keyspace) ||
- readCommand.serializedAtEpoch() == null) // don't try to catch up with pre-5.0 nodes
+ readCommand.serializedAtEpoch() == null) // don't try to catch up with pre-5.0 nodes
return metadata;
Keyspace ks = metadata.schema.getKeyspace(readCommand.metadata().keyspace);
@@ -153,8 +85,8 @@ public class ReadCommandVerbHandler implements IVerbHandler<ReadCommand>
{
TCMMetrics.instance.coordinatorBehindSchema.mark();
throw new CoordinatorBehindException(String.format("Coordinator schema for %s.%s with epoch %s is behind our schema %s",
- message.payload.metadata().keyspace,
- message.payload.metadata().name,
+ readCommand.metadata().keyspace,
+ readCommand.metadata().name,
readCommand.serializedAtEpoch(),
localComparisonEpoch));
}
@@ -164,9 +96,9 @@ public class ReadCommandVerbHandler implements IVerbHandler<ReadCommand>
return metadata;
}
- private ClusterMetadata checkTokenOwnership(ClusterMetadata metadata, Message<ReadCommand> message)
+ private ClusterMetadata checkTokenOwnership(ClusterMetadata metadata, Message<T> message)
{
- ReadCommand command = message.payload;
+ ReadCommand command = getCommand(message.payload);
if (command.metadata().isVirtual())
return metadata;
@@ -181,7 +113,7 @@ public class ReadCommandVerbHandler implements IVerbHandler<ReadCommand>
if (command instanceof SinglePartitionReadCommand)
{
- Token token = ((SinglePartitionReadCommand)command).partitionKey().getToken();
+ Token token = ((SinglePartitionReadCommand) command).partitionKey().getToken();
Replica localReplica = getLocalReplica(metadata, token, command.metadata().keyspace);
if (localReplica == null)
{
@@ -192,7 +124,7 @@ public class ReadCommandVerbHandler implements IVerbHandler<ReadCommand>
{
StorageService.instance.incOutOfRangeOperationCount();
Keyspace.open(command.metadata().keyspace).metric.outOfRangeTokenReads.inc();
- throw InvalidRoutingException.forTokenRead(message.from(), token, metadata.epoch, message.payload);
+ throw InvalidRoutingException.forTokenRead(message.from(), token, metadata.epoch, command);
}
if (!command.acceptsTransient() && localReplica.isTransient())
@@ -206,7 +138,7 @@ public class ReadCommandVerbHandler implements IVerbHandler<ReadCommand>
}
else
{
- AbstractBounds<PartitionPosition> range = ((PartitionRangeReadCommand) command).dataRange().keyRange();
+ AbstractBounds<PartitionPosition> range = command.dataRange().keyRange();
// TODO: preexisting issue: for the range queries or queries that span multiple replicas, we can only make requests where the right token is owned, but not the left one
Replica maxTokenLocalReplica = getLocalReplica(metadata, range.right.getToken(), command.metadata().keyspace);
@@ -219,10 +151,9 @@ public class ReadCommandVerbHandler implements IVerbHandler<ReadCommand>
{
StorageService.instance.incOutOfRangeOperationCount();
Keyspace.open(command.metadata().keyspace).metric.outOfRangeTokenReads.inc();
- throw InvalidRoutingException.forRangeRead(message.from(), range, metadata.epoch, message.payload);
+ throw InvalidRoutingException.forRangeRead(message.from(), range, metadata.epoch, command);
}
-
// TODO: preexisting issue: we should change the whole range for transient-ness, not just the right token
if (command.acceptsTransient() != maxTokenLocalReplica.isTransient())
{
@@ -239,10 +170,10 @@ public class ReadCommandVerbHandler implements IVerbHandler<ReadCommand>
private static Replica getLocalReplica(ClusterMetadata metadata, Token token, String keyspace)
{
return metadata.placements
- .get(metadata.schema.getKeyspaces().getNullable(keyspace).params.replication)
- .reads
- .forToken(token)
- .get()
- .lookup(FBUtilities.getBroadcastAddressAndPort());
+ .get(metadata.schema.getKeyspaces().getNullable(keyspace).params.replication)
+ .reads
+ .forToken(token)
+ .get()
+ .lookup(FBUtilities.getBroadcastAddressAndPort());
}
}
diff --git a/src/java/org/apache/cassandra/db/CounterMutation.java b/src/java/org/apache/cassandra/db/CounterMutation.java
index 52068473e5..be4b9951e7 100644
--- a/src/java/org/apache/cassandra/db/CounterMutation.java
+++ b/src/java/org/apache/cassandra/db/CounterMutation.java
@@ -53,6 +53,7 @@ import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.locator.AbstractReplicationStrategy;
+import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.service.CacheService;
import org.apache.cassandra.tracing.Tracing;
@@ -64,6 +65,7 @@ import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static org.apache.cassandra.net.MessagingService.VERSION_40;
import static org.apache.cassandra.net.MessagingService.VERSION_50;
import static org.apache.cassandra.net.MessagingService.VERSION_60;
+import static org.apache.cassandra.net.MessagingService.VERSION_61;
import static org.apache.cassandra.utils.Clock.Global.nanoTime;
public class CounterMutation implements IMutation
@@ -81,6 +83,18 @@ public class CounterMutation implements IMutation
this.consistency = consistency;
}
+ @Override
+ public MutationId id()
+ {
+ return mutation.id();
+ }
+
+ @Override
+ public CounterMutation withMutationId(MutationId mutationId)
+ {
+ return new CounterMutation(mutation.withMutationId(mutationId), consistency);
+ }
+
public String getKeyspaceName()
{
return mutation.getKeyspaceName();
@@ -148,7 +162,7 @@ public class CounterMutation implements IMutation
*/
public Mutation applyCounterMutation() throws WriteTimeoutException
{
- Mutation.PartitionUpdateCollector resultBuilder = new Mutation.PartitionUpdateCollector(getKeyspaceName(), key());
+ Mutation.PartitionUpdateCollector resultBuilder = new Mutation.PartitionUpdateCollector(id(), getKeyspaceName(), key());
Keyspace keyspace = Keyspace.open(getKeyspaceName());
List<Lock> locks = new ArrayList<>();
@@ -374,6 +388,7 @@ public class CounterMutation implements IMutation
private int serializedSize40;
private int serializedSize50;
private int serializedSize51;
+ private int serializedSize52;
public int serializedSize(int version)
{
@@ -391,6 +406,10 @@ public class CounterMutation implements IMutation
if (serializedSize51 == 0)
serializedSize51 = (int) serializer.serializedSize(this, VERSION_60);
return serializedSize51;
+ case VERSION_61:
+ if (serializedSize52 == 0)
+ serializedSize52 = (int) serializer.serializedSize(this, VERSION_61);
+ return serializedSize52;
default:
throw new IllegalStateException("Unknown serialization version: " + version);
}
diff --git a/src/java/org/apache/cassandra/db/IMutation.java b/src/java/org/apache/cassandra/db/IMutation.java
index f497a2aea5..f0116b58ff 100644
--- a/src/java/org/apache/cassandra/db/IMutation.java
+++ b/src/java/org/apache/cassandra/db/IMutation.java
@@ -27,6 +27,7 @@ import javax.annotation.Nullable;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts;
import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.service.ClientState;
@@ -34,6 +35,8 @@ public interface IMutation
{
long MAX_MUTATION_SIZE = DatabaseDescriptor.getMaxMutationSize();
+ MutationId id();
+ IMutation withMutationId(MutationId mutationId);
void apply();
String getKeyspaceName();
Collection<TableId> getTableIds();
diff --git a/src/java/org/apache/cassandra/db/Keyspace.java b/src/java/org/apache/cassandra/db/Keyspace.java
index e0cdf8b5c2..ff9a52fefe 100644
--- a/src/java/org/apache/cassandra/db/Keyspace.java
+++ b/src/java/org/apache/cassandra/db/Keyspace.java
@@ -33,6 +33,7 @@ import java.util.concurrent.locks.Lock;
import java.util.stream.Stream;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import org.slf4j.Logger;
@@ -44,6 +45,7 @@ import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.lifecycle.SSTableSet;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.db.repair.CassandraKeyspaceRepairManager;
+import org.apache.cassandra.db.tracked.TrackedKeyspaceWriteHandler;
import org.apache.cassandra.db.view.ViewManager;
import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry;
import org.apache.cassandra.exceptions.WriteTimeoutException;
@@ -54,6 +56,7 @@ import org.apache.cassandra.io.util.File;
import org.apache.cassandra.locator.AbstractReplicationStrategy;
import org.apache.cassandra.metrics.KeyspaceMetrics;
import org.apache.cassandra.repair.KeyspaceRepairManager;
+import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.SchemaConstants;
@@ -106,6 +109,7 @@ public class Keyspace
public final ViewManager viewManager;
private final KeyspaceWriteHandler writeHandler;
+ private final TrackedKeyspaceWriteHandler trackedWriteHandler;
private final KeyspaceRepairManager repairManager;
private final SchemaProvider schema;
private final String name;
@@ -285,6 +289,7 @@ public class Keyspace
this.repairManager = new CassandraKeyspaceRepairManager(this);
this.writeHandler = new CassandraKeyspaceWriteHandler(this);
+ this.trackedWriteHandler = new TrackedKeyspaceWriteHandler();
}
public Keyspace(KeyspaceMetadata metadata)
@@ -296,6 +301,7 @@ public class Keyspace
this.viewManager = new ViewManager(this);
this.repairManager = new CassandraKeyspaceRepairManager(this);
this.writeHandler = new CassandraKeyspaceWriteHandler(this);
+ this.trackedWriteHandler = new TrackedKeyspaceWriteHandler();
this.metadataRef = new KeyspaceMetadataRef(metadata, schema);
}
@@ -395,13 +401,9 @@ public class Keyspace
public Future<?> applyFuture(Mutation mutation, boolean writeCommitLog, boolean updateIndexes)
{
- return applyInternal(mutation, writeCommitLog, updateIndexes, true, true, new AsyncPromise<>());
- }
-
- public Future<?> applyFuture(Mutation mutation, boolean writeCommitLog, boolean updateIndexes, boolean isDroppable,
- boolean isDeferrable)
- {
- return applyInternal(mutation, writeCommitLog, updateIndexes, isDroppable, isDeferrable, new AsyncPromise<>());
+ return getMetadata().useMutationTracking()
+ ? applyInternalTracked(mutation, new AsyncPromise<>())
+ : applyInternal(mutation, writeCommitLog, updateIndexes, true, true, new AsyncPromise<>());
}
public void apply(Mutation mutation, boolean writeCommitLog, boolean updateIndexes)
@@ -431,7 +433,10 @@ public class Keyspace
boolean updateIndexes,
boolean isDroppable)
{
- applyInternal(mutation, makeDurable, updateIndexes, isDroppable, false, null);
+ if (getMetadata().useMutationTracking())
+ applyInternalTracked(mutation, null);
+ else
+ applyInternal(mutation, makeDurable, updateIndexes, isDroppable, false, null);
}
/**
@@ -451,6 +456,8 @@ public class Keyspace
boolean isDeferrable,
Promise<?> future)
{
+ Preconditions.checkState(!getMetadata().useMutationTracking() && mutation.id().isNone());
+
if (TEST_FAIL_WRITES && getMetadata().name.equals(TEST_FAIL_WRITES_KS))
throw new RuntimeException("Testing write failures");
@@ -598,6 +605,41 @@ public class Keyspace
}
}
+ /**
+ * Append the mutation to the mutation journal, then update memtables and indexes.
+ */
+ private Future<?> applyInternalTracked(Mutation mutation, Promise<?> future)
+ {
+ Preconditions.checkState(getMetadata().useMutationTracking() && !mutation.id().isNone());
+
+ if (TEST_FAIL_WRITES && getMetadata().name.equals(TEST_FAIL_WRITES_KS))
+ throw new RuntimeException("Testing write failures");
+
+ try (WriteContext ctx = trackedWriteHandler.beginWrite(mutation, true))
+ {
+ MutationTrackingService.instance.startWriting(mutation);
+
+ for (PartitionUpdate upd : mutation.getPartitionUpdates())
+ {
+ ColumnFamilyStore cfs = Schema.instance.getColumnFamilyStoreInstance(upd.metadata().id);
+ if (cfs == null)
+ {
+ logger.error("Attempting to mutate non-existant table {} ({}.{})", upd.metadata().id, upd.metadata().keyspace, upd.metadata().name);
+ continue;
+ }
+
+ cfs.getWriteHandler().write(upd, ctx, true);
+ }
+ }
+
+ MutationTrackingService.instance.finishWriting(mutation);
+
+ if (future != null)
+ future.trySuccess(null);
+
+ return future;
+ }
+
public AbstractReplicationStrategy getReplicationStrategy()
{
return getMetadata().replicationStrategy;
diff --git a/src/java/org/apache/cassandra/db/Mutation.java b/src/java/org/apache/cassandra/db/Mutation.java
index 9fc49ae15e..e1a01c117f 100644
--- a/src/java/org/apache/cassandra/db/Mutation.java
+++ b/src/java/org/apache/cassandra/db/Mutation.java
@@ -52,6 +52,7 @@ import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.io.util.TeeDataInputPlus;
import org.apache.cassandra.locator.ReplicaPlan;
import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
@@ -63,6 +64,7 @@ import static com.google.common.base.Preconditions.checkState;
import static org.apache.cassandra.net.MessagingService.VERSION_40;
import static org.apache.cassandra.net.MessagingService.VERSION_50;
import static org.apache.cassandra.net.MessagingService.VERSION_60;
+import static org.apache.cassandra.net.MessagingService.VERSION_61;
import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime;
public class Mutation implements IMutation, Supplier<Mutation>
@@ -71,6 +73,7 @@ public class Mutation implements IMutation, Supplier<Mutation>
public static final int ALLOW_POTENTIAL_TRANSACTION_CONFLICTS = 0x01;
+ private final MutationId id;
// todo this is redundant
// when we remove it, also restore SerializationsTest.testMutationRead to not regenerate new Mutations each test
private final String keyspaceName;
@@ -103,23 +106,29 @@ public class Mutation implements IMutation, Supplier<Mutation>
// because it is being applied by one or in a context where transaction conflicts don't occur
private PotentialTxnConflicts potentialTxnConflicts;
+ public Mutation(MutationId id, PartitionUpdate update)
+ {
+ this(id, update.metadata().keyspace, update.partitionKey(), ImmutableMap.of(update.metadata().id, update), approxTime.now(), update.metadata().params.cdc, PotentialTxnConflicts.DISALLOW);
+ }
+
public Mutation(PartitionUpdate update)
{
- this(update, PotentialTxnConflicts.DISALLOW);
+ this(MutationId.none(), update, PotentialTxnConflicts.DISALLOW);
}
- public Mutation(PartitionUpdate update, PotentialTxnConflicts potentialTxnConflicts)
+ public Mutation(MutationId id, PartitionUpdate update, PotentialTxnConflicts potentialTxnConflicts)
{
- this(update.metadata().keyspace, update.partitionKey(), ImmutableMap.of(update.metadata().id, update), approxTime.now(), update.metadata().params.cdc, potentialTxnConflicts);
+ this(id, update.metadata().keyspace, update.partitionKey(), ImmutableMap.of(update.metadata().id, update), approxTime.now(), update.metadata().params.cdc, potentialTxnConflicts);
}
- public Mutation(String keyspaceName, DecoratedKey key, ImmutableMap<TableId, PartitionUpdate> modifications, long approxCreatedAtNanos, PotentialTxnConflicts potentialTxnConflicts)
+ public Mutation(MutationId id, String keyspaceName, DecoratedKey key, ImmutableMap<TableId, PartitionUpdate> modifications, long approxCreatedAtNanos, PotentialTxnConflicts potentialTxnConflicts)
{
- this(keyspaceName, key, modifications, approxCreatedAtNanos, cdcEnabled(modifications.values()), potentialTxnConflicts);
+ this(id, keyspaceName, key, modifications, approxCreatedAtNanos, cdcEnabled(modifications.values()), potentialTxnConflicts);
}
- public Mutation(String keyspaceName, DecoratedKey key, ImmutableMap<TableId, PartitionUpdate> modifications, long approxCreatedAtNanos, boolean cdcEnabled, PotentialTxnConflicts potentialTxnConflicts)
+ public Mutation(MutationId id, String keyspaceName, DecoratedKey key, ImmutableMap<TableId, PartitionUpdate> modifications, long approxCreatedAtNanos, boolean cdcEnabled, PotentialTxnConflicts potentialTxnConflicts)
{
+ this.id = id;
this.keyspaceName = keyspaceName;
this.key = key;
this.modifications = modifications;
@@ -128,6 +137,18 @@ public class Mutation implements IMutation, Supplier<Mutation>
this.potentialTxnConflicts = potentialTxnConflicts;
}
+ @Override
+ public MutationId id()
+ {
+ return id;
+ }
+
+ @Override
+ public Mutation withMutationId(MutationId mutationId)
+ {
+ return new Mutation(mutationId, keyspaceName, key, modifications, approxCreatedAtNanos, cdcEnabled, potentialTxnConflicts);
+ }
+
private static boolean cdcEnabled(Iterable<PartitionUpdate> modifications)
{
boolean cdc = false;
@@ -159,7 +180,7 @@ public class Mutation implements IMutation, Supplier<Mutation>
Map<TableId, PartitionUpdate> updates = builder.build();
checkState(!updates.isEmpty(), "Updates should not be empty");
- return new Mutation(keyspaceName, key, builder.build(), approxCreatedAtNanos, potentialTxnConflicts);
+ return new Mutation(id, keyspaceName, key, builder.build(), approxCreatedAtNanos, potentialTxnConflicts);
}
public @Nullable Mutation without(TableId tableId)
@@ -258,6 +279,8 @@ public class Mutation implements IMutation, Supplier<Mutation>
throw new IllegalArgumentException("Can't merge mutations with differing policies on allowing potential transaction conflicts");
potentialTxnConflicts = mutation.potentialTxnConflicts;
updatedTables.addAll(mutation.modifications.keySet());
+ if (!mutation.id().isNone())
+ throw new IllegalArgumentException();
if (ks != null && !ks.equals(mutation.keyspaceName))
throw new IllegalArgumentException();
if (key != null && !key.equals(mutation.key))
@@ -283,7 +306,7 @@ public class Mutation implements IMutation, Supplier<Mutation>
modifications.put(table, updates.size() == 1 ? updates.get(0) : PartitionUpdate.merge(updates));
updates.clear();
}
- return new Mutation(ks, key, modifications.build(), approxTime.now(), potentialTxnConflicts);
+ return new Mutation(MutationId.none(), ks, key, modifications.build(), approxTime.now(), potentialTxnConflicts);
}
public Future<?> applyFuture()
@@ -399,6 +422,7 @@ public class Mutation implements IMutation, Supplier<Mutation>
private int serializedSize40;
private int serializedSize50;
private int serializedSize51;
+ private int serializedSize52;
public int serializedSize(int version)
{
@@ -416,6 +440,10 @@ public class Mutation implements IMutation, Supplier<Mutation>
if (serializedSize51 == 0)
serializedSize51 = (int) serializer.serializedSize(this, VERSION_60);
return serializedSize51;
+ case VERSION_61:
+ if (serializedSize52 == 0)
+ serializedSize52 = (int) serializer.serializedSize(this, VERSION_61);
+ return serializedSize52;
default:
throw new IllegalStateException("Unknown serialization version: " + version);
}
@@ -428,9 +456,9 @@ public class Mutation implements IMutation, Supplier<Mutation>
* @param partitionKey the key of partition this if a mutation for.
* @return a newly created builder.
*/
- public static SimpleBuilder simpleBuilder(String keyspaceName, DecoratedKey partitionKey)
+ public static SimpleBuilder simpleBuilder(MutationId mutationId, String keyspaceName, DecoratedKey partitionKey)
{
- return new SimpleBuilders.MutationBuilder(keyspaceName, partitionKey);
+ return new SimpleBuilders.MutationBuilder(mutationId, keyspaceName, partitionKey);
}
/**
@@ -554,9 +582,9 @@ public class Mutation implements IMutation, Supplier<Mutation>
}
static void serializeInternal(PartitionUpdate.PartitionUpdateSerializer serializer,
- Mutation mutation,
- DataOutputPlus out,
- int version) throws IOException
+ Mutation mutation,
+ DataOutputPlus out,
+ int version) throws IOException
{
Map<TableId, PartitionUpdate> modifications = mutation.modifications;
@@ -567,6 +595,9 @@ public class Mutation implements IMutation, Supplier<Mutation>
out.write(flags);
}
+ if (version >= MessagingService.VERSION_61)
+ MutationId.serializer.serialize(mutation.id, out, version);
+
/* serialize the modifications in the mutation */
int size = modifications.size();
out.writeUnsignedVInt32(size);
@@ -592,13 +623,18 @@ public class Mutation implements IMutation, Supplier<Mutation>
int flags = teeIn.readByte();
potentialTxnConflicts = potentialTxnConflicts(flags);
}
+
+ MutationId id = version >= MessagingService.VERSION_61
+ ? MutationId.serializer.deserialize(teeIn, version)
+ : MutationId.none();
+
int size = teeIn.readUnsignedVInt32();
assert size > 0;
PartitionUpdate update = PartitionUpdate.serializer.deserialize(teeIn, version, flag);
if (size == 1)
{
- m = new Mutation(update, potentialTxnConflicts);
+ m = new Mutation(id, update, potentialTxnConflicts);
}
else
{
@@ -611,7 +647,7 @@ public class Mutation implements IMutation, Supplier<Mutation>
update = PartitionUpdate.serializer.deserialize(teeIn, version, flag);
modifications.put(update.metadata().id, update);
}
- m = new Mutation(update.metadata().keyspace, dk, modifications.build(), approxTime.now(), potentialTxnConflicts);
+ m = new Mutation(id, update.metadata().keyspace, dk, modifications.build(), approxTime.now(), potentialTxnConflicts);
}
//Only cache serializations that don't hit the limit
@@ -691,6 +727,8 @@ public class Mutation implements IMutation, Supplier<Mutation>
{
if (version >= VERSION_60)
size += TypeSizes.sizeof((byte)ALLOW_POTENTIAL_TRANSACTION_CONFLICTS); // flags
+ if (version >= MessagingService.VERSION_61)
+ size += MutationId.serializer.serializedSize(mutation.id, version);
size += TypeSizes.sizeofUnsignedVInt(mutation.modifications.size());
for (PartitionUpdate partitionUpdate : mutation.modifications.values())
size += serializer.serializedSize(partitionUpdate, version);
@@ -706,20 +744,22 @@ public class Mutation implements IMutation, Supplier<Mutation>
public static class PartitionUpdateCollector
{
private final ImmutableMap.Builder<TableId, PartitionUpdate> modifications = new ImmutableMap.Builder<>();
+ private final MutationId mutationId;
private final String keyspaceName;
private final DecoratedKey key;
private final long approxCreatedAtNanos = approxTime.now();
private boolean empty = true;
- private PotentialTxnConflicts potentialTxnConflicts;
+ private final PotentialTxnConflicts potentialTxnConflicts;
- public PartitionUpdateCollector(String keyspaceName, DecoratedKey key)
+ public PartitionUpdateCollector(MutationId mutationId, String keyspaceName, DecoratedKey key)
{
- this(keyspaceName, key, PotentialTxnConflicts.DISALLOW);
+ this(mutationId, keyspaceName, key, PotentialTxnConflicts.DISALLOW);
}
- public PartitionUpdateCollector(String keyspaceName, DecoratedKey key, PotentialTxnConflicts potentialTxnConflicts)
+ public PartitionUpdateCollector(MutationId mutationId, String keyspaceName, DecoratedKey key, PotentialTxnConflicts potentialTxnConflicts)
{
+ this.mutationId = mutationId;
this.keyspaceName = keyspaceName;
this.key = key;
this.potentialTxnConflicts = potentialTxnConflicts;
@@ -756,7 +796,7 @@ public class Mutation implements IMutation, Supplier<Mutation>
public Mutation build()
{
- return new Mutation(keyspaceName, key, modifications.build(), approxCreatedAtNanos, potentialTxnConflicts);
+ return new Mutation(mutationId, keyspaceName, key, modifications.build(), approxCreatedAtNanos, potentialTxnConflicts);
}
}
}
diff --git a/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java b/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java
index d49cf986cb..43bb60d0d4 100644
--- a/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java
+++ b/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java
@@ -51,10 +51,14 @@ import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.metrics.TableMetrics;
import org.apache.cassandra.net.Verb;
+import org.apache.cassandra.replication.MutationSummary;
+import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.StorageProxy;
import org.apache.cassandra.service.reads.ReadCoordinator;
+import org.apache.cassandra.service.reads.tracked.PartialTrackedRangeRead;
+import org.apache.cassandra.service.reads.tracked.PartialTrackedRead;
import org.apache.cassandra.tcm.Epoch;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.transport.Dispatcher;
@@ -89,6 +93,11 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR
this.requestedSlices = dataRange.clusteringIndexFilter.getSlices(metadata());
}
+ public Slices requestedSlices()
+ {
+ return requestedSlices;
+ }
+
private static PartitionRangeReadCommand create(Epoch serializedAtEpoch,
boolean isDigest,
int digestVersion,
@@ -469,6 +478,22 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR
}
}
+ @Override
+ protected PartialTrackedRead createInProgressRead(UnfilteredPartitionIterator iterator,
+ ReadExecutionController executionController,
+ Index.Searcher searcher,
+ ColumnFamilyStore cfs,
+ long startTimeNanos)
+ {
+ return PartialTrackedRangeRead.create(executionController, searcher, cfs, startTimeNanos, this, iterator);
+ }
+
+ @Override
+ protected MutationSummary createMutationSummaryInternal(boolean includePending)
+ {
+ return MutationTrackingService.instance.createSummaryForRange(dataRange.keyRange, metadata().id, includePending);
+ }
+
@Override
protected boolean intersects(SSTableReader sstable)
{
diff --git a/src/java/org/apache/cassandra/db/ReadCommand.java b/src/java/org/apache/cassandra/db/ReadCommand.java
index f5f9105602..edd5aad08e 100644
--- a/src/java/org/apache/cassandra/db/ReadCommand.java
+++ b/src/java/org/apache/cassandra/db/ReadCommand.java
@@ -89,6 +89,7 @@ import org.apache.cassandra.net.MessageFlag;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.ParamType;
import org.apache.cassandra.net.Verb;
+import org.apache.cassandra.replication.MutationSummary;
import org.apache.cassandra.schema.IndexMetadata;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.SchemaConstants;
@@ -99,6 +100,7 @@ import org.apache.cassandra.service.ActiveRepairService;
import org.apache.cassandra.service.ClientWarn;
import org.apache.cassandra.service.accord.serializers.TableMetadatas;
import org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter;
+import org.apache.cassandra.service.reads.tracked.PartialTrackedRead;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.Epoch;
import org.apache.cassandra.tracing.Tracing;
@@ -122,10 +124,15 @@ import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime;
* General interface for storage-engine read commands (common to both range and
* single partition commands).
* <p>
- * This contains all the informations needed to do a local read.
+ * This contains all the information needed to do a local read.
*/
public abstract class ReadCommand extends AbstractReadQuery
{
+ private interface ReadCompleter<T>
+ {
+ T complete(UnfilteredPartitionIterator iterator, ReadExecutionController executionController, Index.Searcher searcher, ColumnFamilyStore cfs, long startTimeNanos);
+ }
+
private static final int TEST_ITERATION_DELAY_MILLIS = CassandraRelevantProperties.TEST_READ_ITERATION_DELAY_MS.getInt();
protected static final Logger logger = LoggerFactory.getLogger(ReadCommand.class);
@@ -428,6 +435,13 @@ public abstract class ReadCommand extends AbstractReadQuery
protected abstract UnfilteredPartitionIterator queryStorage(ColumnFamilyStore cfs, ReadExecutionController executionController);
+ protected abstract MutationSummary createMutationSummaryInternal(boolean includePending);
+
+ public MutationSummary createMutationSummary(boolean includePending)
+ {
+ return createMutationSummaryInternal(includePending);
+ }
+
/**
* Whether the underlying {@code ClusteringIndexFilter} is reversed or not.
*
@@ -489,24 +503,10 @@ public abstract class ReadCommand extends AbstractReadQuery
selectOptions.validate(metadata(), IndexRegistry.obtain(metadata()), indexQueryPlan());
}
- /**
- * Executes this command on the local host.
- *
- * @param executionController the execution controller spanning this command
- *
- * @return an iterator over the result of executing this command locally.
- */
- // iterators created inside the try as long as we do close the original resultIterator), or by closing the result.
- public UnfilteredPartitionIterator executeLocally(ReadExecutionController executionController)
- {
- return executeLocally(executionController, null);
- }
-
// ClusterMetadata is null on startup when there are local reads from system tables before it's initialized
- public UnfilteredPartitionIterator executeLocally(ReadExecutionController executionController, @Nullable ClusterMetadata cm)
+ private <T> T beginRead(ReadExecutionController executionController, @Nullable ClusterMetadata cm, ReadCompleter<T> completer)
{
long startTimeNanos = nanoTime();
-
COMMAND.set(this);
try
{
@@ -531,57 +531,70 @@ public abstract class ReadCommand extends AbstractReadQuery
.collect(Collectors.joining(",")));
}
+ if (searcher != null && metadata().replicationType().isTracked())
+ throw new UnsupportedOperationException("TODO: support tracked index reads");
+
UnfilteredPartitionIterator iterator = (null == searcher) ? queryStorage(cfs, executionController) : searcher.search(executionController);
+
+ return completer.complete(iterator, executionController, searcher, cfs, startTimeNanos);
+ }
+ finally
+ {
+ COMMAND.set(null);
+ }
+ }
+
+ private UnfilteredPartitionIterator completeRead(UnfilteredPartitionIterator iterator, ReadExecutionController executionController, Index.Searcher searcher, ColumnFamilyStore cfs, long startTimeNanos)
+ {
+ COMMAND.set(this);
+ try
+ {
iterator = RTBoundValidator.validate(iterator, Stage.MERGED, false);
+ iterator = withQuerySizeTracking(iterator);
+ iterator = maybeSlowDownForTesting(iterator);
+ iterator = withQueryCancellation(iterator);
+ iterator = maybeRecordPurgeableTombstones(iterator, cfs);
+ iterator = RTBoundValidator.validate(withoutPurgeableTombstones(iterator, cfs, executionController), Stage.PURGED, false);
+ iterator = withMetricsRecording(iterator, cfs.metric, startTimeNanos);
- try
- {
- iterator = withQuerySizeTracking(iterator);
- iterator = maybeSlowDownForTesting(iterator);
- iterator = withQueryCancellation(iterator);
- iterator = maybeRecordPurgeableTombstones(iterator, cfs);
- iterator = RTBoundValidator.validate(withoutPurgeableTombstones(iterator, cfs, executionController), Stage.PURGED, false);
- iterator = withMetricsRecording(iterator, cfs.metric, startTimeNanos);
-
- // If we've used a 2ndary index, we know the result already satisfy the primary expression used, so
- // no point in checking it again.
- RowFilter filter = (null == searcher) ? rowFilter() : indexQueryPlan.postIndexQueryFilter();
-
- /*
- * TODO: We'll currently do filtering by the rowFilter here because it's convenient. However,
- * we'll probably want to optimize by pushing it down the layer (like for dropped columns) as it
- * would be more efficient (the sooner we discard stuff we know we don't care, the less useless
- * processing we do on it).
- */
- iterator = filter.filter(iterator, nowInSec());
-
- // apply the limits/row counter; this transformation is stopping and would close the iterator as soon
- // as the count is observed; if that happens in the middle of an open RT, its end bound will not be included.
- // If tracking repaired data, the counter is needed for overreading repaired data, otherwise we can
- // optimise the case where this.limit = DataLimits.NONE which skips an unnecessary transform
- if (executionController.isTrackingRepairedStatus())
- {
- DataLimits.Counter limit =
- limits().newCounter(nowInSec(), false, selectsFullPartition(), metadata().enforceStrictLiveness());
- iterator = limit.applyTo(iterator);
- // ensure that a consistent amount of repaired data is read on each replica. This causes silent
- // overreading from the repaired data set, up to limits(). The extra data is not visible to
- // the caller, only iterated to produce the repaired data digest.
- iterator = executionController.getRepairedDataInfo().extend(iterator, limit);
- }
- else
- {
- iterator = limits().filter(iterator, nowInSec(), selectsFullPartition());
- }
+ // If we've used a 2ndary index, we know the result already satisfy the primary expression used, so
+ // no point in checking it again.
+ RowFilter filter = (null == searcher) ? rowFilter() : indexQueryPlan.postIndexQueryFilter();
- // because of the above, we need to append an aritifical end bound if the source iterator was stopped short by a counter.
- return RTBoundCloser.close(iterator);
+ /*
+ * TODO: We'll currently do filtering by the rowFilter here because it's convenient. However,
+ * we'll probably want to optimize by pushing it down the layer (like for dropped columns) as it
+ * would be more efficient (the sooner we discard stuff we know we don't care, the less useless
+ * processing we do on it).
+ */
+ iterator = filter.filter(iterator, nowInSec());
+
+ // apply the limits/row counter; this transformation is stopping and would close the iterator as soon
+ // as the count is observed; if that happens in the middle of an open RT, its end bound will not be included.
+ // If tracking repaired data, the counter is needed for overreading repaired data, otherwise we can
+ // optimise the case where this.limit = DataLimits.NONE which skips an unnecessary transform
+ if (executionController.isTrackingRepairedStatus())
+ {
+ DataLimits.Counter limit =
+ limits().newCounter(nowInSec(), false, selectsFullPartition(), metadata().enforceStrictLiveness());
+ iterator = limit.applyTo(iterator);
+ // ensure that a consistent amount of repaired data is read on each replica. This causes silent
+ // overreading from the repaired data set, up to limits(). The extra data is not visible to
+ // the caller, only iterated to produce the repaired data digest.
+ iterator = executionController.getRepairedDataInfo().extend(iterator, limit);
}
- catch (RuntimeException | Error e)
+ else
{
- iterator.close();
- throw e;
+ iterator = limits().filter(iterator, nowInSec(), selectsFullPartition());
}
+
+ // because of the above, we need to append an aritifical end bound if the source iterator was stopped short by a counter.
+ return RTBoundCloser.close(iterator);
+ }
+ catch (RuntimeException | Error e)
+ {
+ iterator.close();
+ throw e;
}
finally
{
@@ -589,6 +602,45 @@ public abstract class ReadCommand extends AbstractReadQuery
}
}
+ /**
+ * Convert the initial iterator into a snapshot of data, either by capturing immutable references or materializing
+ * the contents of the iterator into memory. Once this method returns, subsequent writes against this table should
+ * not be reflected in the result of the InProgressRead
+ */
+ protected abstract PartialTrackedRead createInProgressRead(UnfilteredPartitionIterator iterator,
+ ReadExecutionController executionController,
+ Index.Searcher searcher,
+ ColumnFamilyStore cfs,
+ long startTimeNanos);
+
+ public PartialTrackedRead beginTrackedRead(ReadExecutionController executionController)
+ {
+ return beginRead(executionController, null, this::createInProgressRead);
+ }
+
+ public UnfilteredPartitionIterator completeTrackedRead(UnfilteredPartitionIterator iterator, PartialTrackedRead read)
+ {
+ return completeRead(iterator, read.executionController(), read.searcher(), read.cfs(), read.startTimeNanos());
+ }
+
+ /**
+ * Executes this command on the local host.
+ *
+ * @param executionController the execution controller spanning this command
+ *
+ * @return an iterator over the result of executing this command locally.
+ */
+ // iterators created inside the try as long as we do close the original resultIterator), or by closing the result.
+ public UnfilteredPartitionIterator executeLocally(ReadExecutionController executionController)
+ {
+ return beginRead(executionController, null, this::completeRead);
+ }
+
+ public UnfilteredPartitionIterator executeLocally(ReadExecutionController executionController, @Nullable ClusterMetadata cm)
+ {
+ return beginRead(executionController, cm, this::completeRead);
+ }
+
protected abstract void recordLatency(TableMetrics metric, long latencyNanos);
public ReadExecutionController executionController(boolean trackRepairedStatus)
diff --git a/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java b/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java
index 8ff89f4e9b..6fe8e063b2 100644
--- a/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java
+++ b/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java
@@ -16,41 +16,32 @@
* limitations under the License.
*/
package org.apache.cassandra.db;
-
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
-import org.apache.cassandra.dht.AbstractBounds;
-import org.apache.cassandra.dht.Token;
-import org.apache.cassandra.exceptions.CoordinatorBehindException;
-import org.apache.cassandra.exceptions.InvalidRequestException;
-import org.apache.cassandra.exceptions.InvalidRoutingException;
import org.apache.cassandra.exceptions.QueryCancelledException;
import org.apache.cassandra.exceptions.RetryOnDifferentSystemException;
-import org.apache.cassandra.locator.Replica;
-import org.apache.cassandra.metrics.TCMMetrics;
-import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessagingService;
-import org.apache.cassandra.schema.SchemaConstants;
-import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.tcm.ClusterMetadata;
-import org.apache.cassandra.tcm.ClusterMetadataService;
-import org.apache.cassandra.tcm.Epoch;
import org.apache.cassandra.tracing.Tracing;
-import org.apache.cassandra.utils.FBUtilities;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static org.apache.cassandra.exceptions.RequestFailureReason.RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM;
-public class ReadCommandVerbHandler implements IVerbHandler<ReadCommand>
+public class ReadCommandVerbHandler extends AbstractReadCommandVerbHandler<ReadCommand>
{
public static final ReadCommandVerbHandler instance = new ReadCommandVerbHandler();
private static final Logger logger = LoggerFactory.getLogger(ReadCommandVerbHandler.class);
+ @Override
+ protected ReadCommand getCommand(ReadCommand payload)
+ {
+ return payload;
+ }
+
public ReadResponse doRead(ReadCommand command, boolean trackRepairedData)
{
ReadResponse response;
@@ -63,25 +54,11 @@ public class ReadCommandVerbHandler implements IVerbHandler<ReadCommand>
return response;
}
- public void doVerb(Message<ReadCommand> message)
+ @Override
+ protected void performRead(Message<ReadCommand> message, ClusterMetadata metadata)
{
- if (message.epoch().isAfter(Epoch.EMPTY))
- {
- ClusterMetadata metadata = ClusterMetadata.current();
- metadata = checkTokenOwnership(metadata, message);
- metadata = checkSchemaVersion(metadata, message);
- }
-
- MessageParams.reset();
-
- long timeout = message.expiresAtNanos() - message.createdAtNanos();
- ReadCommand command = message.payload;
- command.setMonitoringTime(message.createdAtNanos(), message.isCrossNode(), timeout, DatabaseDescriptor.getSlowQueryTimeout(NANOSECONDS));
-
- if (message.trackWarnings())
- command.trackWarnings();
-
ReadResponse response;
+ ReadCommand command = message.payload;
try
{
response = doRead(command, message.trackRepairedData());
@@ -132,117 +109,4 @@ public class ReadCommandVerbHandler implements IVerbHandler<ReadCommand>
MessagingService.instance().metrics.recordDroppedMessage(message, message.elapsedSinceCreated(NANOSECONDS), NANOSECONDS);
}
}
-
- private ClusterMetadata checkSchemaVersion(ClusterMetadata metadata, Message<ReadCommand> message)
- {
- ReadCommand readCommand = message.payload;
-
- if (SchemaConstants.isSystemKeyspace(readCommand.metadata().keyspace) ||
- readCommand.serializedAtEpoch() == null) // don't try to catch up with pre-5.0 nodes
- return metadata;
-
- Keyspace ks = metadata.schema.getKeyspace(readCommand.metadata().keyspace);
- ColumnFamilyStore cfs = ks != null ? ks.getColumnFamilyStore(readCommand.metadata().id) : null;
- Epoch localComparisonEpoch = metadata.epoch;
- if (cfs != null)
- localComparisonEpoch = cfs.metadata().epoch;
-
- if (localComparisonEpoch.isBefore(readCommand.serializedAtEpoch()))
- metadata = ClusterMetadataService.instance().fetchLogFromPeerOrCMS(metadata, message.from(), message.epoch());
- else if (localComparisonEpoch.isAfter(readCommand.serializedAtEpoch()))
- {
- TCMMetrics.instance.coordinatorBehindSchema.mark();
- throw new CoordinatorBehindException(String.format("Coordinator schema for %s.%s with epoch %s is behind our schema %s",
- message.payload.metadata().keyspace,
- message.payload.metadata().name,
- readCommand.serializedAtEpoch(),
- localComparisonEpoch));
- }
- ks = metadata.schema.getKeyspace(readCommand.metadata().keyspace);
- if (ks == null || ks.getColumnFamilyStore(readCommand.metadata().id) == null)
- throw new IllegalStateException("Unknown table " + readCommand.metadata().id +" after fetching remote log entries");
- return metadata;
- }
-
- private ClusterMetadata checkTokenOwnership(ClusterMetadata metadata, Message<ReadCommand> message)
- {
- ReadCommand command = message.payload;
-
- if (command.metadata().isVirtual())
- return metadata;
-
- // Some read commands may be sent using an older Epoch intentionally so validating using the current Epoch
- // doesn't work
- if (command.potentialTxnConflicts().allowed)
- return metadata;
-
- if (command.isTopK())
- return metadata;
-
- if (command instanceof SinglePartitionReadCommand)
- {
- Token token = ((SinglePartitionReadCommand)command).partitionKey().getToken();
- Replica localReplica = getLocalReplica(metadata, token, command.metadata().keyspace);
- if (localReplica == null)
- {
- metadata = ClusterMetadataService.instance().fetchLogFromPeerOrCMS(metadata, message.from(), message.epoch());
- localReplica = getLocalReplica(metadata, token, command.metadata().keyspace);
- }
- if (localReplica == null)
- {
- StorageService.instance.incOutOfRangeOperationCount();
- Keyspace.open(command.metadata().keyspace).metric.outOfRangeTokenReads.inc();
- throw InvalidRoutingException.forTokenRead(message.from(), token, metadata.epoch, message.payload);
- }
-
- if (!command.acceptsTransient() && localReplica.isTransient())
- {
- MessagingService.instance().metrics.recordDroppedMessage(message, message.elapsedSinceCreated(NANOSECONDS), NANOSECONDS);
- throw new InvalidRequestException(String.format("Attempted to serve %s data request from %s node in %s",
- command.acceptsTransient() ? "transient" : "full",
- localReplica.isTransient() ? "transient" : "full",
- this));
- }
- }
- else
- {
- AbstractBounds<PartitionPosition> range = ((PartitionRangeReadCommand) command).dataRange().keyRange();
-
- // TODO: preexisting issue: for the range queries or queries that span multiple replicas, we can only make requests where the right token is owned, but not the left one
- Replica maxTokenLocalReplica = getLocalReplica(metadata, range.right.getToken(), command.metadata().keyspace);
- if (maxTokenLocalReplica == null)
- {
- metadata = ClusterMetadataService.instance().fetchLogFromPeerOrCMS(metadata, message.from(), message.epoch());
- maxTokenLocalReplica = getLocalReplica(metadata, range.right.getToken(), command.metadata().keyspace);
- }
- if (maxTokenLocalReplica == null)
- {
- StorageService.instance.incOutOfRangeOperationCount();
- Keyspace.open(command.metadata().keyspace).metric.outOfRangeTokenReads.inc();
- throw InvalidRoutingException.forRangeRead(message.from(), range, metadata.epoch, message.payload);
- }
-
-
- // TODO: preexisting issue: we should change the whole range for transient-ness, not just the right token
- if (command.acceptsTransient() != maxTokenLocalReplica.isTransient())
- {
- MessagingService.instance().metrics.recordDroppedMessage(message, message.elapsedSinceCreated(NANOSECONDS), NANOSECONDS);
- throw new InvalidRequestException(String.format("Attempted to serve %s data request from %s node in %s",
- command.acceptsTransient() ? "transient" : "full",
- maxTokenLocalReplica.isTransient() ? "transient" : "full",
- this));
- }
- }
- return metadata;
- }
-
- private static Replica getLocalReplica(ClusterMetadata metadata, Token token, String keyspace)
- {
- return metadata.placements
- .get(metadata.schema.getKeyspaces().getNullable(keyspace).params.replication)
- .reads
- .forToken(token)
- .get()
- .lookup(FBUtilities.getBroadcastAddressAndPort());
- }
}
diff --git a/src/java/org/apache/cassandra/db/ReadExecutionController.java b/src/java/org/apache/cassandra/db/ReadExecutionController.java
index 8a62ea390d..85b17d4690 100644
--- a/src/java/org/apache/cassandra/db/ReadExecutionController.java
+++ b/src/java/org/apache/cassandra/db/ReadExecutionController.java
@@ -21,6 +21,7 @@ import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
import org.apache.cassandra.db.filter.DataLimits;
import org.apache.cassandra.index.Index;
@@ -69,6 +70,7 @@ public class ReadExecutionController implements AutoCloseable
if (trackRepairedStatus)
{
+ Preconditions.checkArgument(!command.metadata().replicationType().isTracked(), "Tracking repaired status is not supported for tracked reads");
DataLimits.Counter repairedReadCount = command.limits().newCounter(command.nowInSec(),
false,
command.selectsFullPartition(),
diff --git a/src/java/org/apache/cassandra/db/SimpleBuilders.java b/src/java/org/apache/cassandra/db/SimpleBuilders.java
index 7139b6c8b2..cc8d38ec32 100644
--- a/src/java/org/apache/cassandra/db/SimpleBuilders.java
+++ b/src/java/org/apache/cassandra/db/SimpleBuilders.java
@@ -39,6 +39,7 @@ import org.apache.cassandra.db.rows.BufferCell;
import org.apache.cassandra.db.rows.Cell;
import org.apache.cassandra.db.rows.CellPath;
import org.apache.cassandra.db.rows.Row;
+import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableId;
@@ -116,6 +117,7 @@ public abstract class SimpleBuilders
public static class MutationBuilder extends AbstractBuilder<Mutation.SimpleBuilder> implements Mutation.SimpleBuilder
{
+ private final MutationId mutationId;
private final String keyspaceName;
private final DecoratedKey key;
@@ -123,8 +125,9 @@ public abstract class SimpleBuilders
private PotentialTxnConflicts potentialTxnConflicts = PotentialTxnConflicts.DISALLOW;
- public MutationBuilder(String keyspaceName, DecoratedKey key)
+ public MutationBuilder(MutationId mutationId, String keyspaceName, DecoratedKey key)
{
+ this.mutationId = mutationId;
this.keyspaceName = keyspaceName;
this.key = key;
}
@@ -163,9 +166,9 @@ public abstract class SimpleBuilders
assert !updateBuilders.isEmpty() : "Cannot create empty mutation";
if (updateBuilders.size() == 1)
- return new Mutation(updateBuilders.values().iterator().next().build(), potentialTxnConflicts);
+ return new Mutation(mutationId, updateBuilders.values().iterator().next().build(), potentialTxnConflicts);
- Mutation.PartitionUpdateCollector mutationBuilder = new Mutation.PartitionUpdateCollector(keyspaceName, key, potentialTxnConflicts);
+ Mutation.PartitionUpdateCollector mutationBuilder = new Mutation.PartitionUpdateCollector(mutationId, keyspaceName, key, potentialTxnConflicts);
for (PartitionUpdateBuilder builder : updateBuilders.values())
mutationBuilder.add(builder.build());
return mutationBuilder.build();
diff --git a/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java b/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java
index b487d011b2..667c8349f9 100644
--- a/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java
+++ b/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java
@@ -57,6 +57,7 @@ import org.apache.cassandra.db.partitions.PartitionIterators;
import org.apache.cassandra.db.partitions.SingletonUnfilteredPartitionIterator;
import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
import org.apache.cassandra.db.rows.BaseRowIterator;
+import org.apache.cassandra.db.partitions.*;
import org.apache.cassandra.db.rows.Cell;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.db.rows.Rows;
@@ -78,12 +79,16 @@ import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.metrics.TableMetrics;
import org.apache.cassandra.net.Verb;
+import org.apache.cassandra.replication.MutationSummary;
+import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.CacheService;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.StorageProxy;
import org.apache.cassandra.service.accord.api.PartitionKey;
+import org.apache.cassandra.service.reads.tracked.PartialTrackedRead;
+import org.apache.cassandra.service.reads.tracked.PartialTrackedSinglePartitionRead;
import org.apache.cassandra.tcm.Epoch;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.transport.Dispatcher;
@@ -561,6 +566,16 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar
return new SingletonUnfilteredPartitionIterator(partition);
}
+ @Override
+ protected PartialTrackedRead createInProgressRead(UnfilteredPartitionIterator iterator,
+ ReadExecutionController executionController,
+ Index.Searcher searcher,
+ ColumnFamilyStore cfs,
+ long startTimeNanos)
+ {
+ return PartialTrackedSinglePartitionRead.create(executionController, searcher, cfs, startTimeNanos, this, iterator);
+ }
+
/**
* Fetch the rows requested if in cache; if not, read it from disk and cache it.
* <p>
@@ -926,6 +941,11 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar
}
}
+ protected MutationSummary createMutationSummaryInternal(boolean includePending)
+ {
+ return MutationTrackingService.instance.createSummaryForKey(partitionKey, metadata().id, includePending);
+ }
+
@Override
protected boolean intersects(SSTableReader sstable)
{
diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogDescriptor.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogDescriptor.java
index c7d3d3f16c..a00ba2094d 100644
--- a/src/java/org/apache/cassandra/db/commitlog/CommitLogDescriptor.java
+++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogDescriptor.java
@@ -64,13 +64,14 @@ public class CommitLogDescriptor
public static final int VERSION_40 = 7;
public static final int VERSION_50 = 8;
public static final int VERSION_60 = 9;
+ public static final int VERSION_61 = 10;
/**
* Increment this number if there is a changes in the commit log disc layout or MessagingVersion changes.
* Note: make sure to handle {@link #getMessagingVersion()}
*/
@VisibleForTesting
- public static final int current_version = DatabaseDescriptor.getStorageCompatibilityMode().isBefore(5) ? VERSION_40 : VERSION_60;
+ public static final int current_version = DatabaseDescriptor.getStorageCompatibilityMode().isBefore(5) ? VERSION_40 : VERSION_61;
final int version;
public final long id;
@@ -228,6 +229,8 @@ public class CommitLogDescriptor
return MessagingService.VERSION_50;
case VERSION_60:
return MessagingService.VERSION_60;
+ case VERSION_61:
+ return MessagingService.VERSION_61;
default:
throw new IllegalStateException("Unknown commitlog version " + version);
}
diff --git a/src/java/org/apache/cassandra/db/commitlog/CommitLogReplayer.java b/src/java/org/apache/cassandra/db/commitlog/CommitLogReplayer.java
index 427510258c..e6762aee5c 100644
--- a/src/java/org/apache/cassandra/db/commitlog/CommitLogReplayer.java
+++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogReplayer.java
@@ -313,7 +313,7 @@ public class CommitLogReplayer implements CommitLogReadHandler
if (commitLogReplayer.shouldReplay(update.metadata().id, new CommitLogPosition(segmentId, entryLocation)))
{
if (newPUCollector == null)
- newPUCollector = new Mutation.PartitionUpdateCollector(mutation.getKeyspaceName(), mutation.key());
+ newPUCollector = new Mutation.PartitionUpdateCollector(mutation.id(), mutation.getKeyspaceName(), mutation.key());
newPUCollector.add(update);
commitLogReplayer.replayedCount.incrementAndGet();
}
diff --git a/src/java/org/apache/cassandra/db/partitions/AbstractPartitionIterator.java b/src/java/org/apache/cassandra/db/partitions/AbstractPartitionIterator.java
new file mode 100644
index 0000000000..5dc4663bbb
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/partitions/AbstractPartitionIterator.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.db.partitions;
+
+import org.apache.cassandra.db.rows.RowIterator;
+import org.apache.cassandra.utils.AbstractIterator;
+
+public abstract class AbstractPartitionIterator extends AbstractIterator<RowIterator> implements PartitionIterator
+{
+}
diff --git a/src/java/org/apache/cassandra/db/partitions/PartitionIterators.java b/src/java/org/apache/cassandra/db/partitions/PartitionIterators.java
index bf65b3392a..569cee34d4 100644
--- a/src/java/org/apache/cassandra/db/partitions/PartitionIterators.java
+++ b/src/java/org/apache/cassandra/db/partitions/PartitionIterators.java
@@ -17,14 +17,20 @@
*/
package org.apache.cassandra.db.partitions;
+import java.io.IOError;
+import java.io.IOException;
import java.util.List;
import org.apache.cassandra.db.EmptyIterators;
import org.apache.cassandra.db.SinglePartitionReadQuery;
+import org.apache.cassandra.db.filter.ColumnFilter;
import org.apache.cassandra.db.rows.RowIterator;
import org.apache.cassandra.db.rows.RowIterators;
import org.apache.cassandra.db.transform.MorePartitions;
import org.apache.cassandra.db.transform.Transformation;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.utils.AbstractIterator;
public abstract class PartitionIterators
@@ -167,4 +173,41 @@ public abstract class PartitionIterators
iterator.close();
}
}
+
+ public static class Serializer
+ {
+ public static void serialize(PartitionIterator iterator, ColumnFilter selection, DataOutputPlus out, int version) throws IOException
+ {
+ while (iterator.hasNext())
+ {
+ try (RowIterator partition = iterator.next())
+ {
+ out.writeBoolean(true);
+ RowIterators.Serializer.serialize(partition, selection, out, version);
+ }
+ }
+ out.writeBoolean(false);
+ }
+
+ public static PartitionIterator deserialize(TableMetadata metadata, ColumnFilter selection, DataInputPlus in, int version) throws IOException
+ {
+ return new AbstractPartitionIterator()
+ {
+ @Override
+ protected RowIterator computeNext()
+ {
+ try
+ {
+ if (in.readBoolean())
+ return RowIterators.Serializer.deserialize(metadata, selection, in, version);
+ return endOfData();
+ }
+ catch (IOException e)
+ {
+ throw new IOError(e);
+ }
+ }
+ };
+ }
+ };
}
diff --git a/src/java/org/apache/cassandra/db/partitions/SimpleBTreePartition.java b/src/java/org/apache/cassandra/db/partitions/SimpleBTreePartition.java
new file mode 100644
index 0000000000..1d04c2c592
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/partitions/SimpleBTreePartition.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.db.partitions;
+
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.DeletionInfo;
+import org.apache.cassandra.db.RegularAndStaticColumns;
+import org.apache.cassandra.db.rows.*;
+import org.apache.cassandra.index.transactions.UpdateTransaction;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.utils.btree.BTree;
+import org.apache.cassandra.utils.btree.UpdateFunction;
+import org.apache.cassandra.utils.memory.HeapCloner;
+
+/**
+ * Single threaded btree partition, no waste or allocation tracking
+ */
+public class SimpleBTreePartition extends AbstractBTreePartition implements UpdateFunction<Row, Row>
+{
+ private final TableMetadata metadata;
+ private final UpdateTransaction indexer;
+ private BTreePartitionData data = BTreePartitionData.EMPTY;
+
+ public SimpleBTreePartition(DecoratedKey partitionKey, TableMetadata metadata, UpdateTransaction indexer)
+ {
+ super(partitionKey);
+ this.metadata = metadata;
+ this.indexer = indexer;
+ }
+
+ @Override
+ protected BTreePartitionData holder()
+ {
+ return data;
+ }
+
+ @Override
+ public TableMetadata metadata()
+ {
+ return metadata;
+ }
+
+ @Override
+ protected boolean canHaveShadowedData()
+ {
+ return true;
+ }
+
+ @Override
+ public void onAllocatedOnHeap(long heapSize)
+ {
+
+ }
+
+ protected BTreePartitionData makeMergedPartition(BTreePartitionData current, PartitionUpdate update)
+ {
+ DeletionInfo newDeletionInfo = merge(current.deletionInfo, update.deletionInfo());
+
+ RegularAndStaticColumns columns = current.columns;
+ RegularAndStaticColumns newColumns = update.columns().mergeTo(columns);
+ Row newStatic = mergeStatic(current.staticRow, update.staticRow());
+
+ Object[] tree = BTree.update(current.tree, update.holder().tree, update.metadata().comparator, this);
+ EncodingStats newStats = current.stats.mergeWith(update.stats());
+
+ return new BTreePartitionData(newColumns, tree, newDeletionInfo, newStatic, newStats);
+ }
+
+ private Row mergeStatic(Row current, Row update)
+ {
+ if (update.isEmpty())
+ return current;
+ if (current.isEmpty())
+ return insert(update);
+
+ return merge(current, update);
+ }
+
+ private DeletionInfo merge(DeletionInfo existing, DeletionInfo update)
+ {
+ if (update.isLive() || !update.mayModify(existing))
+ return existing;
+
+ if (!update.getPartitionDeletion().isLive())
+ indexer.onPartitionDeletion(update.getPartitionDeletion());
+
+ if (update.hasRanges())
+ update.rangeIterator(false).forEachRemaining(indexer::onRangeTombstone);
+
+ // Like for rows, we have to clone the update in case internal buffers (when it has range tombstones) reference
+ // memory we shouldn't hold into. But we don't ever store this off-heap currently so we just default to the
+ // HeapAllocator (rather than using 'allocator').
+ DeletionInfo newInfo = existing.mutableCopy().add(update.clone(HeapCloner.instance));
+ return newInfo;
+ }
+
+ public Row insert(Row insert)
+ {
+ indexer.onInserted(insert);
+ return insert;
+ }
+
+ public Row merge(Row existing, Row update)
+ {
+ Row reconciled = Rows.merge(existing, update, ColumnData.noOp);
+ indexer.onUpdated(existing, reconciled);
+
+ return reconciled;
+ }
+
+ public void update(PartitionUpdate update)
+ {
+ data = makeMergedPartition(data, update);
+ }
+}
diff --git a/src/java/org/apache/cassandra/db/rows/AbstractRowIterator.java b/src/java/org/apache/cassandra/db/rows/AbstractRowIterator.java
new file mode 100644
index 0000000000..3481063596
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/rows/AbstractRowIterator.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.db.rows;
+
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.RegularAndStaticColumns;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.utils.AbstractIterator;
+
+public abstract class AbstractRowIterator extends AbstractIterator<Row> implements RowIterator
+{
+ protected final TableMetadata metadata;
+ protected final DecoratedKey key;
+ protected final RegularAndStaticColumns columns;
+ protected final boolean isReversedOrder;
+ protected final Row staticRow;
+
+ public AbstractRowIterator(TableMetadata metadata, DecoratedKey key, RegularAndStaticColumns columns, boolean isReversedOrder, Row staticRow)
+ {
+ this.metadata = metadata;
+ this.key = key;
+ this.columns = columns;
+ this.isReversedOrder = isReversedOrder;
+ this.staticRow = staticRow;
+ }
+
+ @Override
+ public TableMetadata metadata()
+ {
+ return metadata;
+ }
+
+ @Override
+ public DecoratedKey partitionKey()
+ {
+ return key;
+ }
+
+ @Override
+ public RegularAndStaticColumns columns()
+ {
+ return columns;
+ }
+
+ @Override
+ public Row staticRow()
+ {
+ return staticRow;
+ }
+
+ @Override
+ public boolean isReverseOrder()
+ {
+ return isReversedOrder;
+ }
+}
diff --git a/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java b/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java
index 062b389ad8..c9543a308b 100644
--- a/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java
+++ b/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java
@@ -220,7 +220,7 @@ public class ComplexColumnData extends ColumnData implements Iterable<Cell<?>>
public ComplexColumnData withOnlyQueriedData(ColumnFilter filter)
{
- return transformAndFilter(complexDeletion, (cell) -> filter.fetchedCellIsQueried(column, cell.path()) ? null : cell);
+ return transformAndFilter(complexDeletion, (cell) -> filter.fetchedCellIsQueried(column, cell.path()) ? cell : null);
}
public ComplexColumnData purgeDataOlderThan(long timestamp)
diff --git a/src/java/org/apache/cassandra/db/rows/RowIterators.java b/src/java/org/apache/cassandra/db/rows/RowIterators.java
index 640cbc8d5b..b398b27522 100644
--- a/src/java/org/apache/cassandra/db/rows/RowIterators.java
+++ b/src/java/org/apache/cassandra/db/rows/RowIterators.java
@@ -17,14 +17,25 @@
*/
package org.apache.cassandra.db.rows;
+import com.google.common.base.Preconditions;
+import net.nicoulaj.compilecommand.annotations.Inline;
+import org.apache.cassandra.db.*;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.schema.ColumnMetadata;
+import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.SearchIterator;
+import org.apache.cassandra.utils.WrappedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.apache.cassandra.db.Digest;
import org.apache.cassandra.db.filter.ColumnFilter;
import org.apache.cassandra.db.transform.Transformation;
import org.apache.cassandra.schema.TableMetadata;
+import java.io.IOError;
+import java.io.IOException;
+
/**
* Static methods to work with row iterators.
*/
@@ -101,4 +112,272 @@ public abstract class RowIterators
}
return Transformation.apply(iterator, new Log());
}
+
+ private static class RowSerializer
+ {
+ /*
+ * Row flags constants.
+ */
+ private final static int END_OF_PARTITION = 0x01; // Signal the end of the partition. Nothing follows a <flags> field with that flag.
+ private final static int HAS_ALL_COLUMNS = 0x02; // Whether the encoded row has all of the columns from the header present.
+ private final static int IS_STATIC = 0x04; // Whether the encoded row is a static. If there is no extended flag, the row is assumed not static.
+
+
+ public static void writeEndOfPartition(DataOutputPlus out) throws IOException
+ {
+ out.writeByte((byte) END_OF_PARTITION);
+ }
+
+ public static void serializeRow(Row row, SerializationHelper helper, DataOutputPlus out, int version)
+ throws IOException
+ {
+ int flags = 0;
+
+ boolean isStatic = row.isStatic();
+ SerializationHeader header = helper.header;
+ Preconditions.checkArgument(!header.isForSSTable());
+ boolean hasAllColumns = row.columnCount() == header.columns(isStatic).size();
+
+ if (isStatic)
+ flags |= IS_STATIC;
+
+ if (hasAllColumns)
+ flags |= HAS_ALL_COLUMNS;
+
+ out.writeByte((byte)flags);
+
+ if (!isStatic)
+ Clustering.serializer.serialize(row.clustering(), out, version, header.clusteringTypes());
+
+ serializeRowBody(row, flags, helper, out);
+ }
+
+ public static void serializeStaticRow(Row row, SerializationHelper helper, DataOutputPlus out, int version) throws IOException
+ {
+ Preconditions.checkArgument(row.isStatic());
+ serializeRow(row, helper, out, version);
+ }
+
+ @Inline
+ private static void serializeRowBody(Row row, int flags, SerializationHelper helper, DataOutputPlus out)
+ throws IOException
+ {
+ boolean isStatic = row.isStatic();
+
+ SerializationHeader header = helper.header;
+ Preconditions.checkState(!header.isForSSTable());
+ Columns headerColumns = header.columns(isStatic);
+
+ if ((flags & HAS_ALL_COLUMNS) == 0)
+ Columns.serializer.serializeSubset(row.columns(), headerColumns, out);
+
+ SearchIterator<ColumnMetadata, ColumnMetadata> si = helper.iterator(isStatic);
+
+ try
+ {
+ row.apply(cd -> {
+ // We can obtain the column for data directly from data.column(). However, if the cell/complex data
+ // originates from a sstable, the column we'll get will have the type used when the sstable was serialized,
+ // and if that type have been recently altered, that may not be the type we want to serialize the column
+ // with. So we use the ColumnMetadata from the "header" which is "current". Also see #11810 for what
+ // happens if we don't do that.
+ ColumnMetadata column = si.next(cd.column());
+ assert column != null : cd.column.toString();
+
+ try
+ {
+ if (cd.column.isSimple())
+ Cell.serializer.serialize((Cell<?>) cd, column, out, LivenessInfo.EMPTY, header);
+ else
+ UnfilteredSerializer.writeComplexColumn((ComplexColumnData) cd, column, false, LivenessInfo.EMPTY, header, out);
+ }
+ catch (IOException e)
+ {
+ throw new WrappedException(e);
+ }
+ });
+ }
+ catch (WrappedException e)
+ {
+ if (e.getCause() instanceof IOException)
+ throw (IOException) e.getCause();
+
+ throw e;
+ }
+ }
+
+ public static Row deserializeStaticRow(DataInputPlus in, SerializationHeader header, DeserializationHelper helper)
+ throws IOException
+ {
+ int flags = in.readUnsignedByte();
+ Preconditions.checkState(!isEndOfPartition(flags));
+ Preconditions.checkState(isStatic(flags));
+ Row.Builder builder = BTreeRow.sortedBuilder();
+ builder.newRow(Clustering.STATIC_CLUSTERING);
+ return deserializeRowBody(in, header, helper, flags, builder);
+ }
+
+ public static Row deserializeRow(DataInputPlus in, SerializationHeader header, DeserializationHelper helper, Row.Builder builder) throws IOException
+ {
+ int flags = in.readUnsignedByte();
+ if (isEndOfPartition(flags))
+ return null;
+ builder.newRow(Clustering.serializer.deserialize(in, helper.version, header.clusteringTypes()));
+ return deserializeRowBody(in, header, helper, flags, builder);
+ }
+
+ public static Row deserializeRowBody(DataInputPlus in,
+ SerializationHeader header,
+ DeserializationHelper helper,
+ int flags,
+ Row.Builder builder)
+ throws IOException
+ {
+ try
+ {
+ boolean isStatic = isStatic(flags);
+ boolean hasAllColumns = (flags & HAS_ALL_COLUMNS) != 0;
+ Columns headerColumns = header.columns(isStatic);
+
+ Preconditions.checkState(!header.isForSSTable());
+
+ Columns columns = hasAllColumns ? headerColumns : Columns.serializer.deserializeSubset(headerColumns, in);
+
+ try
+ {
+ DataInputPlus finalIn = in;
+ columns.apply(column -> {
+ try
+ {
+ if (column.isSimple())
+ UnfilteredSerializer.readSimpleColumn(column, finalIn, header, helper, builder, LivenessInfo.EMPTY);
+ else
+ UnfilteredSerializer.readComplexColumn(column, finalIn, header, helper, false, builder, LivenessInfo.EMPTY);
+ }
+ catch (IOException e)
+ {
+ throw new WrappedException(e);
+ }
+ });
+ }
+ catch (WrappedException e)
+ {
+ if (e.getCause() instanceof IOException)
+ throw (IOException) e.getCause();
+
+ throw e;
+ }
+
+ return builder.build();
+ }
+ catch (RuntimeException | AssertionError e)
+ {
+ // Corrupted data could be such that it triggers an assertion in the row Builder, or break one of its assumption.
+ // Of course, a bug in said builder could also trigger this, but it's impossible a priori to always make the distinction
+ // between a real bug and data corrupted in just the bad way. Besides, re-throwing as an IOException doesn't hide the
+ // exception, it just make we catch it properly and mark the sstable as corrupted.
+ throw new IOException("Error building row with data deserialized from " + in, e);
+ }
+ }
+
+ public static boolean isEndOfPartition(int flags)
+ {
+ return (flags & END_OF_PARTITION) != 0;
+ }
+
+ public static boolean isStatic(int extendedFlags)
+ {
+ return (extendedFlags & IS_STATIC) != 0;
+ }
+ }
+
+ public static class Serializer
+ {
+ public static final int IS_EMPTY = 0x01;
+ private static final int IS_REVERSED = 0x02;
+ private static final int HAS_STATIC_ROW = 0x04;
+
+ public static void serialize(RowIterator iterator, ColumnFilter selection, DataOutputPlus out, int version) throws IOException
+ {
+ ByteBufferUtil.writeWithVIntLength(iterator.partitionKey().getKey(), out);
+
+ SerializationHeader header = new SerializationHeader(false,
+ iterator.metadata(),
+ iterator.columns(),
+ EncodingStats.NO_STATS);
+
+ int flags = 0;
+ if (iterator.isReverseOrder())
+ flags |= IS_REVERSED;
+
+ if (iterator.isEmpty())
+ {
+ out.writeByte(flags | IS_EMPTY);
+ return;
+ }
+
+ Row staticRow = iterator.staticRow();
+ boolean hasStatic = staticRow != Rows.EMPTY_STATIC_ROW;
+ if (hasStatic)
+ flags |= HAS_STATIC_ROW;
+
+ out.writeByte((byte)flags);
+
+ SerializationHeader.serializer.serializeForMessaging(header, selection, out, hasStatic);
+ SerializationHelper helper = new SerializationHelper(header);
+
+ if (hasStatic)
+ RowSerializer.serializeStaticRow(staticRow, helper, out, version);
+
+ while (iterator.hasNext())
+ RowSerializer.serializeRow(iterator.next(), helper, out, version);
+ RowSerializer.writeEndOfPartition(out);
+ }
+
+ public static RowIterator deserialize(TableMetadata metadata, ColumnFilter selection, DataInputPlus in, int version) throws IOException
+ {
+ DecoratedKey key = metadata.partitioner.decorateKey(ByteBufferUtil.readWithVIntLength(in));
+ int flags = in.readUnsignedByte();
+ boolean isReversed = (flags & IS_REVERSED) != 0;
+ if ((flags & IS_EMPTY) != 0)
+ {
+ return EmptyIterators.row(metadata, key, isReversed);
+ }
+
+ boolean hasStatic = (flags & HAS_STATIC_ROW) != 0;
+
+ SerializationHeader header = SerializationHeader.serializer.deserializeForMessaging(in, metadata, selection, hasStatic);
+
+ DeserializationHelper helper = new DeserializationHelper(metadata, version, DeserializationHelper.Flag.FROM_REMOTE);
+ Row staticRow = hasStatic ? RowSerializer.deserializeStaticRow(in, header, helper) : Rows.EMPTY_STATIC_ROW;
+
+ return new AbstractRowIterator(metadata, key, header.columns(), isReversed, staticRow)
+ {
+ private final Row.Builder builder = BTreeRow.sortedBuilder();
+
+ @Override
+ protected Row computeNext()
+ {
+ try
+ {
+ Row row = RowSerializer.deserializeRow(in, header, helper, builder);
+ return row != null ? row : endOfData();
+ }
+ catch (IOException e)
+ {
+ throw new IOError(e);
+ }
+ }
+
+ @Override
+ public void close()
+ {
+ while (hasNext())
+ next();
+
+ super.close();
+ }
+ };
+ }
+ };
}
diff --git a/src/java/org/apache/cassandra/db/rows/UnfilteredSerializer.java b/src/java/org/apache/cassandra/db/rows/UnfilteredSerializer.java
index 112c1e4c6b..7c80c463cf 100644
--- a/src/java/org/apache/cassandra/db/rows/UnfilteredSerializer.java
+++ b/src/java/org/apache/cassandra/db/rows/UnfilteredSerializer.java
@@ -279,7 +279,7 @@ public class UnfilteredSerializer
}
}
- private static void writeComplexColumn(ComplexColumnData data, ColumnMetadata column, boolean hasComplexDeletion, LivenessInfo rowLiveness, SerializationHeader header, DataOutputPlus out)
+ static void writeComplexColumn(ComplexColumnData data, ColumnMetadata column, boolean hasComplexDeletion, LivenessInfo rowLiveness, SerializationHeader header, DataOutputPlus out)
throws IOException
{
if (hasComplexDeletion)
@@ -661,7 +661,7 @@ public class UnfilteredSerializer
}
}
- private void readSimpleColumn(ColumnMetadata column, DataInputPlus in, SerializationHeader header, DeserializationHelper helper, Row.Builder builder, LivenessInfo rowLiveness)
+ static void readSimpleColumn(ColumnMetadata column, DataInputPlus in, SerializationHeader header, DeserializationHelper helper, Row.Builder builder, LivenessInfo rowLiveness)
throws IOException
{
if (helper.includes(column))
@@ -676,7 +676,7 @@ public class UnfilteredSerializer
}
}
- private void readComplexColumn(ColumnMetadata column, DataInputPlus in, SerializationHeader header, DeserializationHelper helper, boolean hasComplexDeletion, Row.Builder builder, LivenessInfo rowLiveness)
+ static void readComplexColumn(ColumnMetadata column, DataInputPlus in, SerializationHeader header, DeserializationHelper helper, boolean hasComplexDeletion, Row.Builder builder, LivenessInfo rowLiveness)
throws IOException
{
if (helper.includes(column))
@@ -733,7 +733,7 @@ public class UnfilteredSerializer
in.skipBytesFully(markerSize);
}
- private void skipComplexColumn(DataInputPlus in, ColumnMetadata column, SerializationHeader header, boolean hasComplexDeletion)
+ private static void skipComplexColumn(DataInputPlus in, ColumnMetadata column, SerializationHeader header, boolean hasComplexDeletion)
throws IOException
{
if (hasComplexDeletion)
diff --git a/src/java/org/apache/cassandra/db/streaming/CassandraStreamReceiver.java b/src/java/org/apache/cassandra/db/streaming/CassandraStreamReceiver.java
index 40931e4295..c448d5edbe 100644
--- a/src/java/org/apache/cassandra/db/streaming/CassandraStreamReceiver.java
+++ b/src/java/org/apache/cassandra/db/streaming/CassandraStreamReceiver.java
@@ -48,6 +48,7 @@ import org.apache.cassandra.dht.Token;
import org.apache.cassandra.io.sstable.ISSTableScanner;
import org.apache.cassandra.io.sstable.SSTableTxnSingleStreamWriter;
import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.service.accord.AccordService;
import org.apache.cassandra.service.accord.AccordTopology;
import org.apache.cassandra.service.accord.IAccordService;
@@ -200,7 +201,7 @@ public class CassandraStreamReceiver implements StreamReceiver
//
// If the CFS has CDC, however, these updates need to be written to the CommitLog
// so they get archived into the cdc_raw folder
- ks.apply(new Mutation(PartitionUpdate.fromIterator(throttledPartitions.next(), filter)),
+ ks.apply(new Mutation(MutationId.fixme(), PartitionUpdate.fromIterator(throttledPartitions.next(), filter)),
writeCDCCommitLog,
true,
false);
diff --git a/src/java/org/apache/cassandra/db/tracked/TrackedKeyspaceWriteHandler.java b/src/java/org/apache/cassandra/db/tracked/TrackedKeyspaceWriteHandler.java
new file mode 100644
index 0000000000..bfd6d593c6
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/tracked/TrackedKeyspaceWriteHandler.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.db.tracked;
+
+import org.apache.cassandra.db.CassandraWriteContext;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.KeyspaceWriteHandler;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.WriteContext;
+import org.apache.cassandra.db.commitlog.CommitLogPosition;
+import org.apache.cassandra.exceptions.RequestExecutionException;
+import org.apache.cassandra.journal.RecordPointer;
+import org.apache.cassandra.replication.MutationJournal;
+import org.apache.cassandra.tracing.Tracing;
+import org.apache.cassandra.utils.concurrent.OpOrder;
+
+public class TrackedKeyspaceWriteHandler implements KeyspaceWriteHandler
+{
+ @Override
+ public WriteContext beginWrite(Mutation mutation, boolean makeDurable) throws RequestExecutionException
+ {
+ OpOrder.Group group = null;
+ try
+ {
+ group = Keyspace.writeOrder.start();
+
+ Tracing.trace("Appending to mutation journal");
+ RecordPointer pointer = MutationJournal.instance.write(mutation.id(), mutation);
+
+ // TODO (preferred): update journal to return CommitLogPosition or otherwise remove requirement to allocate second object here
+ return new CassandraWriteContext(group, new CommitLogPosition(pointer.segment, pointer.position));
+ }
+ catch (Throwable t)
+ {
+ if (group != null)
+ group.close();
+ throw t;
+ }
+ }
+
+ @Override
+ public WriteContext createContextForIndexing()
+ {
+ return createEmptyContext();
+ }
+
+ @Override
+ public WriteContext createContextForRead()
+ {
+ return createEmptyContext();
+ }
+
+ private WriteContext createEmptyContext()
+ {
+ OpOrder.Group group = Keyspace.writeOrder.start();
+ try
+ {
+ return new CassandraWriteContext(group, null);
+ }
+ catch (Throwable t)
+ {
+ if (group != null)
+ group.close();
+ throw t;
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/db/view/TableViews.java b/src/java/org/apache/cassandra/db/view/TableViews.java
index 0f6d877e73..d7b31053e1 100644
--- a/src/java/org/apache/cassandra/db/view/TableViews.java
+++ b/src/java/org/apache/cassandra/db/view/TableViews.java
@@ -41,6 +41,7 @@ import org.apache.cassandra.db.DeletionInfo;
import org.apache.cassandra.db.DeletionTime;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.db.RangeTombstone;
import org.apache.cassandra.db.ReadExecutionController;
import org.apache.cassandra.db.ReadQuery;
@@ -555,7 +556,7 @@ public class TableViews extends AbstractCollection<View>
Collection<PartitionUpdate> updates = generator.generateViewUpdates();
List<Mutation> mutations = new ArrayList<>(updates.size());
for (PartitionUpdate update : updates)
- mutations.add(new Mutation(update));
+ mutations.add(new Mutation(MutationId.fixme(), update));
generator.clear();
return mutations;
@@ -570,7 +571,7 @@ public class TableViews extends AbstractCollection<View>
Mutation.PartitionUpdateCollector collector = mutations.get(key);
if (collector == null)
{
- collector = new Mutation.PartitionUpdateCollector(baseTableMetadata.keyspace, key);
+ collector = new Mutation.PartitionUpdateCollector(MutationId.none(), baseTableMetadata.keyspace, key);
mutations.put(key, collector);
}
collector.add(update);
diff --git a/src/java/org/apache/cassandra/db/virtual/VirtualMutation.java b/src/java/org/apache/cassandra/db/virtual/VirtualMutation.java
index 929ead029f..4aa718462c 100644
--- a/src/java/org/apache/cassandra/db/virtual/VirtualMutation.java
+++ b/src/java/org/apache/cassandra/db/virtual/VirtualMutation.java
@@ -43,6 +43,7 @@ import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.NoPayload;
+import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.service.ClientState;
@@ -98,6 +99,18 @@ public final class VirtualMutation implements IMutation
this.modifications = modifications;
}
+ @Override
+ public MutationId id()
+ {
+ return MutationId.none();
+ }
+
+ @Override
+ public IMutation withMutationId(MutationId mutationId)
+ {
+ throw new IllegalArgumentException("MutationId cannot be used for virtual mutations");
+ }
+
@Override
public void apply()
{
diff --git a/src/java/org/apache/cassandra/hints/HintsDescriptor.java b/src/java/org/apache/cassandra/hints/HintsDescriptor.java
index 5b8db34035..a049ec6cdc 100644
--- a/src/java/org/apache/cassandra/hints/HintsDescriptor.java
+++ b/src/java/org/apache/cassandra/hints/HintsDescriptor.java
@@ -71,7 +71,8 @@ final class HintsDescriptor
static final int VERSION_40 = 2;
static final int VERSION_50 = 3;
static final int VERSION_60 = 4;
- static final int CURRENT_VERSION = DatabaseDescriptor.getStorageCompatibilityMode().isBefore(5) ? VERSION_40 : VERSION_60;
+ static final int VERSION_61 = 5;
+ static final int CURRENT_VERSION = DatabaseDescriptor.getStorageCompatibilityMode().isBefore(5) ? VERSION_40 : VERSION_61;
static final String COMPRESSION = "compression";
static final String ENCRYPTION = "encryption";
@@ -261,6 +262,8 @@ final class HintsDescriptor
return MessagingService.VERSION_50;
case VERSION_60:
return MessagingService.VERSION_60;
+ case VERSION_61:
+ return MessagingService.VERSION_61;
default:
throw new AssertionError();
}
diff --git a/src/java/org/apache/cassandra/journal/Journal.java b/src/java/org/apache/cassandra/journal/Journal.java
index 4db2f7bfd8..29972bb20d 100644
--- a/src/java/org/apache/cassandra/journal/Journal.java
+++ b/src/java/org/apache/cassandra/journal/Journal.java
@@ -206,8 +206,11 @@ public class Journal<K, V> implements Shutdownable
public void start()
{
+ if (state.get() == State.NORMAL)
+ return;
+
Invariants.require(state.compareAndSet(State.UNINITIALIZED, State.INITIALIZING),
- "Unexpected journal state during initialization", state);
+ "Unexpected journal state during initialization: %s", state);
metrics.register(flusher);
deleteTmpFiles();
@@ -301,7 +304,7 @@ public class Journal<K, V> implements Shutdownable
releaser.awaitTermination(1, TimeUnit.MINUTES);
metrics.deregister();
Invariants.require(state.compareAndSet(State.SHUTDOWN, State.TERMINATED),
- "Unexpected journal state while trying to shut down", state);
+ "Unexpected journal state while trying to shut down: %s", state);
}
catch (InterruptedException e)
{
diff --git a/src/java/org/apache/cassandra/metrics/ReadRepairMetrics.java b/src/java/org/apache/cassandra/metrics/ReadRepairMetrics.java
index 67d3a56ca7..28d2a91f82 100644
--- a/src/java/org/apache/cassandra/metrics/ReadRepairMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/ReadRepairMetrics.java
@@ -49,6 +49,7 @@ public class ReadRepairMetrics
*/
public static final Meter repairedBlockingFromAccord = Metrics.meter(factory.createMetricName("RepairedBlockingFromAccord"));
public static final Meter reconcileRead = Metrics.meter(factory.createMetricName("ReconcileRead"));
+ public static final Meter trackedReconcile = Metrics.meter(factory.createMetricName("TrackedReconcile"));
/** @deprecated See CASSANDRA-13910 */
@Deprecated(since = "4.0")
diff --git a/src/java/org/apache/cassandra/metrics/TableMetrics.java b/src/java/org/apache/cassandra/metrics/TableMetrics.java
index 9e00988da7..356e9c8e92 100644
--- a/src/java/org/apache/cassandra/metrics/TableMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/TableMetrics.java
@@ -59,6 +59,7 @@ import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
import org.apache.cassandra.metrics.Sampler.SamplerType;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.SchemaConstants;
+import org.apache.cassandra.service.reads.ReplicaFilteringProtection;
import org.apache.cassandra.utils.EstimatedHistogram;
import org.apache.cassandra.utils.ExpMovingAverage;
import org.apache.cassandra.utils.MovingAverage;
@@ -377,7 +378,7 @@ public class TableMetrics
public final Meter replicaFilteringProtectionRequests;
/**
- * This histogram records the maximum number of rows {@link org.apache.cassandra.service.reads.ReplicaFilteringProtection}
+ * This histogram records the maximum number of rows {@link ReplicaFilteringProtection}
* caches at a point in time per query. With no replica divergence, this is equivalent to the maximum number of
* cached rows in a single partition during a query. It can be helpful when choosing appropriate values for the
* replica_filtering_protection thresholds in cassandra.yaml.
diff --git a/src/java/org/apache/cassandra/net/Message.java b/src/java/org/apache/cassandra/net/Message.java
index 98ecca9ef5..81001fcf59 100644
--- a/src/java/org/apache/cassandra/net/Message.java
+++ b/src/java/org/apache/cassandra/net/Message.java
@@ -61,6 +61,7 @@ import static org.apache.cassandra.db.TypeSizes.sizeofUnsignedVInt;
import static org.apache.cassandra.net.MessagingService.VERSION_40;
import static org.apache.cassandra.net.MessagingService.VERSION_50;
import static org.apache.cassandra.net.MessagingService.VERSION_60;
+import static org.apache.cassandra.net.MessagingService.VERSION_61;
import static org.apache.cassandra.utils.FBUtilities.getBroadcastAddressAndPort;
import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime;
import static org.apache.cassandra.utils.vint.VIntCoding.computeUnsignedVIntSize;
@@ -246,6 +247,12 @@ public class Message<T> implements ResponseContext
return outWithParam(nextId(), verb, 0, payload, flag.addTo(0), null, null);
}
+ public static <T> Message<T> outWithParam(Verb verb, T payload, ParamType paramType, Object paramValue)
+ {
+ assert !verb.isResponse() : verb;
+ return outWithParam(nextId(), verb, payload, paramType, paramValue);
+ }
+
public static <T> Message<T> outWithFlags(Verb verb, T payload, MessageFlag flag1, MessageFlag flag2)
{
assert !verb.isResponse();
@@ -1263,6 +1270,7 @@ public class Message<T> implements ResponseContext
private int serializedSize40;
private int serializedSize50;
private int serializedSize51;
+ private int serializedSize52;
/**
* Serialized size of the entire message, for the provided messaging version. Caches the calculated value.
@@ -1283,6 +1291,10 @@ public class Message<T> implements ResponseContext
if (serializedSize51 == 0)
serializedSize51 = serializer.serializedSize(this, VERSION_60);
return serializedSize51;
+ case VERSION_61:
+ if (serializedSize52 == 0)
+ serializedSize52 = serializer.serializedSize(this, VERSION_61);
+ return serializedSize52;
default:
throw new IllegalStateException("Unknown serialization version " + version);
}
@@ -1290,7 +1302,8 @@ public class Message<T> implements ResponseContext
private int payloadSize40 = -1;
private int payloadSize50 = -1;
- private int payloadSize51 = -1;
+ private int payloadSize60 = -1;
+ private int payloadSize61 = -1;
private int payloadSize(int version)
{
@@ -1305,9 +1318,13 @@ public class Message<T> implements ResponseContext
payloadSize50 = serializer.payloadSize(this, VERSION_50);
return payloadSize50;
case VERSION_60:
- if (payloadSize51 < 0)
- payloadSize51 = serializer.payloadSize(this, VERSION_60);
- return payloadSize51;
+ if (payloadSize60 < 0)
+ payloadSize60 = serializer.payloadSize(this, VERSION_60);
+ return payloadSize60;
+ case VERSION_61:
+ if (payloadSize61 < 0)
+ payloadSize61 = serializer.payloadSize(this, VERSION_61);
+ return payloadSize61;
default:
throw new IllegalStateException("Unkown serialization version " + version);
diff --git a/src/java/org/apache/cassandra/net/MessagingService.java b/src/java/org/apache/cassandra/net/MessagingService.java
index f7208f694e..4d567d4d11 100644
--- a/src/java/org/apache/cassandra/net/MessagingService.java
+++ b/src/java/org/apache/cassandra/net/MessagingService.java
@@ -225,7 +225,9 @@ public class MessagingService extends MessagingServiceMBeanImpl implements Messa
// c14227 TTL overflow, 'uint' timestamps
VERSION_50(13),
// TCM, index hints
- VERSION_60(14);
+ VERSION_60(14),
+ // Mutation Tracking
+ VERSION_61(15);
public static final Version MIN_ACCORD_VERSION = Version.VERSION_60;
@@ -266,8 +268,9 @@ public class MessagingService extends MessagingServiceMBeanImpl implements Messa
public static final int VERSION_40 = 12;
public static final int VERSION_50 = 13; // c14227 TTL overflow, 'uint' timestamps
public static final int VERSION_60 = 14; // TCM, index hints
+ public static final int VERSION_61 = 15; // Mutation Tracking
public static final int minimum_version = VERSION_40;
- public static final int maximum_version = VERSION_60;
+ public static final int maximum_version = VERSION_61;
// we want to use a modified behavior for the tools and clients - that is, since they are not running a server, they
// should not need to run in a compatibility mode. They should be able to connect to the server regardless whether
// it uses messaving version 4 or 5
@@ -309,7 +312,7 @@ public class MessagingService extends MessagingServiceMBeanImpl implements Messa
private static Version currentVersion()
{
- return DatabaseDescriptor.getStorageCompatibilityMode().isBefore(5) ? Version.VERSION_40 : Version.VERSION_60;
+ return DatabaseDescriptor.getStorageCompatibilityMode().isBefore(5) ? Version.VERSION_40 : Version.VERSION_61;
}
private static class MSHandle
diff --git a/src/java/org/apache/cassandra/net/Verb.java b/src/java/org/apache/cassandra/net/Verb.java
index ad2317e5ab..3969a27de7 100644
--- a/src/java/org/apache/cassandra/net/Verb.java
+++ b/src/java/org/apache/cassandra/net/Verb.java
@@ -131,6 +131,12 @@ import org.apache.cassandra.service.paxos.cleanup.PaxosStartPrepareCleanup;
import org.apache.cassandra.service.paxos.cleanup.PaxosUpdateLowBallot;
import org.apache.cassandra.service.paxos.v1.PrepareVerbHandler;
import org.apache.cassandra.service.paxos.v1.ProposeVerbHandler;
+import org.apache.cassandra.service.reads.tracked.ReadReconcileNotify;
+import org.apache.cassandra.service.reads.tracked.ReadReconcileReceive;
+import org.apache.cassandra.service.reads.tracked.ReadReconcileSend;
+import org.apache.cassandra.service.reads.tracked.TrackedDataResponse;
+import org.apache.cassandra.service.reads.tracked.TrackedRead;
+import org.apache.cassandra.service.reads.tracked.TrackedSummaryResponse;
import org.apache.cassandra.streaming.DataMovement;
import org.apache.cassandra.streaming.DataMovementVerbHandler;
import org.apache.cassandra.streaming.ReplicationDoneVerbHandler;
@@ -314,6 +320,18 @@ public enum Verb
TCM_FETCH_PEER_LOG_RSP (818, P0, shortTimeout, FETCH_METADATA, MessageSerializers::logStateSerializer, RESPONSE_HANDLER ),
TCM_FETCH_PEER_LOG_REQ (819, P0, rpcTimeout, FETCH_METADATA, () -> FetchPeerLog.serializer, () -> FetchPeerLog.Handler.instance, TCM_FETCH_PEER_LOG_RSP ),
+ // tracked replication
+ READ_RECONCILE_SEND (901, P0, rpcTimeout, READ, () -> ReadReconcileSend.serializer, () -> ReadReconcileSend.verbHandler ),
+ READ_RECONCILE_RCV (902, P0, rpcTimeout, MUTATION, () -> ReadReconcileReceive.serializer, () -> ReadReconcileReceive.verbHandler ),
+ READ_RECONCILE_NOTIFY (903, P0, rpcTimeout, REQUEST_RESPONSE, () -> ReadReconcileNotify.serializer, () -> ReadReconcileNotify.verbHandler ),
+
+ TRACKED_PARTITION_READ_RSP (906, P2, readTimeout, REQUEST_RESPONSE, () -> TrackedDataResponse.serializer, RESPONSE_HANDLER ),
+ TRACKED_PARTITION_READ_REQ (907, P3, readTimeout, READ, () -> TrackedRead.DataRequest.serializer, () -> TrackedRead.verbHandler, TRACKED_PARTITION_READ_RSP),
+ TRACKED_RANGE_READ_RSP (908, P2, rangeTimeout, REQUEST_RESPONSE, () -> TrackedDataResponse.serializer, RESPONSE_HANDLER ),
+ TRACKED_RANGE_READ_REQ (909, P3, rangeTimeout, READ, () -> TrackedRead.DataRequest.serializer, () -> TrackedRead.verbHandler, TRACKED_RANGE_READ_RSP ),
+ TRACKED_SUMMARY_RSP (910, P2, readTimeout, REQUEST_RESPONSE, () -> TrackedSummaryResponse.serializer, () -> TrackedSummaryResponse.verbHandler ),
+ TRACKED_SUMMARY_REQ (911, P3, readTimeout, READ, () -> TrackedRead.SummaryRequest.serializer, () -> TrackedRead.verbHandler, TRACKED_SUMMARY_RSP ),
+
INITIATE_DATA_MOVEMENTS_RSP (814, P1, rpcTimeout, MISC, () -> NoPayload.serializer, RESPONSE_HANDLER ),
INITIATE_DATA_MOVEMENTS_REQ (815, P1, rpcTimeout, MISC, () -> DataMovement.serializer, () -> DataMovementVerbHandler.instance, INITIATE_DATA_MOVEMENTS_RSP ),
DATA_MOVEMENT_EXECUTED_RSP (816, P1, rpcTimeout, MISC, () -> NoPayload.serializer, RESPONSE_HANDLER ),
diff --git a/src/java/org/apache/cassandra/replication/CoordinatorLog.java b/src/java/org/apache/cassandra/replication/CoordinatorLog.java
new file mode 100644
index 0000000000..0abd0db93e
--- /dev/null
+++ b/src/java/org/apache/cassandra/replication/CoordinatorLog.java
@@ -0,0 +1,240 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.replication;
+
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.PartitionPosition;
+import org.apache.cassandra.dht.AbstractBounds;
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.schema.TableId;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis;
+
+public abstract class CoordinatorLog
+{
+ private static final Logger logger = LoggerFactory.getLogger(CoordinatorLog.class);
+
+ protected final int localHostId;
+ protected final CoordinatorLogId logId;
+ protected final Participants participants;
+
+ /**
+ * State machines and an Id <-> token index for unreconciled mutation ids that exist oh this host.
+ */
+ private final LocalMutationStates unreconciledMutations;
+
+ protected final Offsets.Mutable[] witnessedIds;
+ protected final Offsets.Mutable reconciledIds;
+ protected final ReadWriteLock lock;
+
+ CoordinatorLog(int localHostId, CoordinatorLogId logId, Participants participants)
+ {
+ this.localHostId = localHostId;
+ this.logId = logId;
+ this.participants = participants;
+ this.unreconciledMutations = new LocalMutationStates();
+ this.lock = new ReentrantReadWriteLock();
+
+ Offsets.Mutable[] ids = new Offsets.Mutable[participants.size()];
+ for (int i = 0; i < participants.size(); i++)
+ ids[i] = new Offsets.Mutable(logId);
+
+ witnessedIds = ids;
+ reconciledIds = new Offsets.Mutable(logId);
+ }
+
+ static CoordinatorLog create(int localHostId, CoordinatorLogId id, Participants participants)
+ {
+ return id.hostId == localHostId ? new CoordinatorLogPrimary(localHostId, id, participants)
+ : new CoordinatorLogReplica(localHostId, id, participants);
+ }
+
+ void witnessedRemoteMutation(MutationId mutationId, int onHostId)
+ {
+ logger.trace("witnessed remote mutation {} from {}", mutationId, onHostId);
+ lock.writeLock().lock();
+ try
+ {
+ if (!get(onHostId).add(mutationId.offset()))
+ return; // already witnessed
+
+ if (!getLocal().contains(mutationId.offset()))
+ return; // local host hasn't witnessed -> no cleanup needed
+
+ // see if any other replicas haven't witnessed the id yet
+ boolean allOtherReplicasWitnessed = true;
+ for (int i = 0; i < participants.size() && allOtherReplicasWitnessed; i++)
+ {
+ int hostId = participants.get(i);
+ if (hostId != onHostId && hostId != localHostId && !get(hostId).contains(mutationId.offset()))
+ allOtherReplicasWitnessed = false;
+ }
+
+ if (allOtherReplicasWitnessed)
+ {
+ logger.trace("marking mutation {} as fully reconciled", mutationId);
+ // if all replicas have now witnessed the id, remove it from the index
+ unreconciledMutations.remove(mutationId.offset());
+ reconciledIds.add(mutationId.offset());
+ }
+ }
+ finally
+ {
+ lock.writeLock().unlock();
+ }
+ }
+
+ void startWriting(Mutation mutation)
+ {
+ lock.writeLock().lock();
+ try
+ {
+ if (getLocal().contains(mutation.id().offset()))
+ return; // already witnessed; shouldn't get to this path often (duplicate mutation)
+
+ unreconciledMutations.startWriting(mutation);
+ }
+ finally
+ {
+ lock.writeLock().unlock();
+ }
+ }
+
+ void finishWriting(Mutation mutation)
+ {
+ logger.trace("witnessed local mutation {}", mutation.id());
+ lock.writeLock().lock();
+ try
+ {
+ if (!getLocal().add(mutation.id().offset()))
+ throw new IllegalStateException("finishWriting() called on a reconciled mutation");
+
+ // see if any other replicas haven't witnessed the id yet
+ boolean allOtherReplicasWitnessed = true;
+ for (int i = 0; i < participants.size() && allOtherReplicasWitnessed; i++)
+ {
+ int hostId = participants.get(i);
+ if (hostId != localHostId && !get(hostId).contains(mutation.id().offset()))
+ allOtherReplicasWitnessed = false;
+ }
+
+ // if some replicas also haven't witnessed the mutation yet, we should update local mutation state;
+ // otherwise we are the last node to witness this mutation, and can clean it up
+ if (allOtherReplicasWitnessed)
+ reconciledIds.add(mutation.id().offset());
+ else
+ unreconciledMutations.finishWriting(mutation);
+ }
+ finally
+ {
+ lock.writeLock().unlock();
+ }
+ }
+
+ /**
+ * Look up unreconciled sequence ids of mutations witnessed by this host in this coordinataor log.
+ * Adds the ids to the supplied collection, so it can be reused to aggregate lookups for multiple logs.
+ */
+ boolean collectOffsetsFor(Token token, TableId tableId, boolean includePending, Offsets.OffsetReciever unreconciledInto, Offsets.OffsetReciever reconciledInto)
+ {
+ lock.readLock().lock();
+ try
+ {
+ reconciledInto.addAll(reconciledIds);
+ return unreconciledMutations.collect(token, tableId, includePending, unreconciledInto);
+ }
+ finally
+ {
+ lock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Look up unreconciled sequence ids of mutations witnessed by this host in this coordinataor log.
+ * Adds the ids to the supplied collection, so it can be reused to aggregate lookups for multiple logs.
+ */
+ boolean collectOffsetsFor(AbstractBounds<PartitionPosition> range, TableId tableId, boolean includePending, Offsets.OffsetReciever unreconciledInto, Offsets.OffsetReciever reconciledInto)
+ {
+ lock.readLock().lock();
+ try
+ {
+ reconciledInto.addAll(reconciledIds);
+ return unreconciledMutations.collect(range, tableId, includePending, unreconciledInto);
+ }
+ finally
+ {
+ lock.readLock().unlock();
+ }
+ }
+
+ protected Offsets.Mutable get(int hostId)
+ {
+ return witnessedIds[participants.indexOf(hostId)];
+ }
+
+ protected Offsets.Mutable getLocal()
+ {
+ return witnessedIds[participants.indexOf(localHostId)];
+ }
+
+ public static class CoordinatorLogPrimary extends CoordinatorLog
+ {
+ AtomicLong sequenceId = new AtomicLong(-1);
+
+ CoordinatorLogPrimary(int localHostId, CoordinatorLogId logId, Participants participants)
+ {
+ super(localHostId, logId, participants);
+ }
+
+ MutationId nextId()
+ {
+ return new MutationId(logId.asLong(), nextSequenceId());
+ }
+
+ private long nextSequenceId()
+ {
+ while (true)
+ {
+ long prev = sequenceId.get();
+ int prevOffset = MutationId.offset(prev);
+ int prevTimestamp = MutationId.timestamp(prev);
+
+ int nextOffset = prevOffset + 1;
+ int nextTimestamp = Math.max(prevTimestamp + 1, (int) (currentTimeMillis() / 1000L));
+ long next = MutationId.sequenceId(nextOffset, nextTimestamp);
+
+ if (sequenceId.compareAndSet(prev, next))
+ return next;
+ }
+ }
+ }
+
+ public static class CoordinatorLogReplica extends CoordinatorLog
+ {
+ CoordinatorLogReplica(int localHostId, CoordinatorLogId logId, Participants participants)
+ {
+ super(localHostId, logId, participants);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/replication/CoordinatorLogId.java b/src/java/org/apache/cassandra/replication/CoordinatorLogId.java
new file mode 100644
index 0000000000..55c38485b0
--- /dev/null
+++ b/src/java/org/apache/cassandra/replication/CoordinatorLogId.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.replication;
+
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Comparator;
+
+public class CoordinatorLogId implements Serializable
+{
+ /** TCM host ID */
+ protected final int hostId;
+
+ /**
+ * Host log ID (unique within the host).
+ * Allocated anew on host restart - one per token range replicated by the host.
+ * Persisted on allocation, unique within the host.
+ */
+ protected final int hostLogId;
+
+ CoordinatorLogId(long id)
+ {
+ this(hostId(id), hostLogId(id));
+ }
+
+ CoordinatorLogId(int hostId, int hostLogId)
+ {
+ this.hostId = hostId;
+ this.hostLogId = hostLogId;
+ }
+
+ public int hostId()
+ {
+ return hostId;
+ }
+
+ public int hostLogId()
+ {
+ return hostLogId;
+ }
+
+ public long asLong()
+ {
+ return asLong(hostId, hostLogId);
+ }
+
+ static long asLong(int hostId, int hostLogId)
+ {
+ return ((long) hostId << 32) | hostLogId;
+ }
+
+ static int hostId(long coordinatorLogId)
+ {
+ return (int) (coordinatorLogId >>> 32);
+ }
+
+ static int hostLogId(long coordinatorLogId)
+ {
+ return (int) coordinatorLogId;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "CoordinatorLogId{" + hostId + ", " + hostLogId + '}';
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (o == null || getClass() != o.getClass()) return false;
+ CoordinatorLogId logId = (CoordinatorLogId) o;
+ return hostId == logId.hostId && hostLogId == logId.hostLogId;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Integer.hashCode(hostLogId) + 31 * Integer.hashCode(hostId);
+ }
+
+ public static final Comparator<CoordinatorLogId> comparator = (l, r) -> Long.compareUnsigned(l.asLong(), r.asLong());
+
+ public static final IVersionedSerializer<CoordinatorLogId> serializer = new IVersionedSerializer<>()
+ {
+ @Override
+ public void serialize(CoordinatorLogId logId, DataOutputPlus out, int version) throws IOException
+ {
+ out.writeInt(logId.hostId);
+ out.writeInt(logId.hostLogId);
+ }
+
+ @Override
+ public CoordinatorLogId deserialize(DataInputPlus in, int version) throws IOException
+ {
+ int hostId = in.readInt();
+ int hostLogId = in.readInt();
+ return new CoordinatorLogId(hostId, hostLogId);
+ }
+
+ @Override
+ public long serializedSize(CoordinatorLogId logId, int version)
+ {
+ return TypeSizes.sizeof(logId.hostId) + TypeSizes.sizeof(logId.hostLogId);
+ }
+ };
+}
diff --git a/src/java/org/apache/cassandra/replication/LocalMutationStates.java b/src/java/org/apache/cassandra/replication/LocalMutationStates.java
new file mode 100644
index 0000000000..9bb1b4d834
--- /dev/null
+++ b/src/java/org/apache/cassandra/replication/LocalMutationStates.java
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.replication;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Sets;
+
+import org.agrona.collections.Int2ObjectHashMap;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.PartitionPosition;
+import org.apache.cassandra.dht.AbstractBounds;
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.schema.TableId;
+
+/**
+ * Tracks unreconciled local mutations - the subset of all unreconciled mutations
+ * that have been witnessed, or are currently being written to, on the local node.
+ */
+class LocalMutationStates
+{
+ private final Int2ObjectHashMap<Entry> statesMap = new Int2ObjectHashMap<>();
+ private final SortedSet<Entry> statesSet = new TreeSet<>(Entry.comparator);
+
+ enum Visibility
+ {
+ PENDING, // written to the journal, but not yet to LSM
+ VISIBLE, // written to both the journal and LSM
+ }
+
+ private static final class Entry
+ {
+ private static final Comparator<Entry> comparator = (left, right) ->
+ {
+ int cmp = left.token.compareTo(right.token);
+ return (cmp != 0) ? cmp : Integer.compare(left.offset, right.offset);
+ };
+
+ final Token token;
+ final int offset;
+ final Object tableOrTables;
+ private Visibility visibility;
+
+ Entry(Token token, int offset, Object tableOrTables, Visibility visibility)
+ {
+ this.token = token;
+ this.offset = offset;
+ this.tableOrTables = tableOrTables;
+ this.visibility = visibility;
+ }
+
+ static Entry create(Mutation mutation)
+ {
+ Collection<TableId> ids = mutation.getTableIds();
+ Preconditions.checkArgument(!ids.isEmpty());
+ return new Entry(mutation.key().getToken(), mutation.id().offset(), tableOrTables(mutation), Visibility.PENDING);
+ }
+
+ private static Object tableOrTables(Mutation mutation)
+ {
+ Collection<TableId> ids = mutation.getTableIds();
+ Preconditions.checkArgument(!ids.isEmpty());
+ return ids.size() == 1 ? ids.iterator().next() : Sets.newHashSet(mutation.getTableIds());
+ }
+
+ boolean contains(TableId tableId)
+ {
+ return tableOrTables instanceof Set
+ ? ((Set<?>) tableOrTables).contains(tableId)
+ : tableId.equals(tableOrTables);
+ }
+
+ boolean isVisible()
+ {
+ return visibility == Visibility.VISIBLE;
+ }
+
+ static Entry start(Token token, boolean isInclusive)
+ {
+ return new Entry(token, isInclusive ? 0 : Integer.MAX_VALUE, null, null);
+ }
+
+ static Entry end(Token token, boolean isInclusive)
+ {
+ return new Entry(token, isInclusive ? Integer.MAX_VALUE : 0, null, null);
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (!(o instanceof Entry))
+ return false;
+ Entry that = (Entry) o;
+ return this.offset == that.offset && this.token.equals(that.token);
+ }
+ }
+
+ void startWriting(Mutation mutation)
+ {
+ Entry entry = Entry.create(mutation);
+ statesMap.put(entry.offset, entry);
+ statesSet.add(entry);
+ }
+
+ void finishWriting(Mutation mutation)
+ {
+ Entry entry = statesMap.get(mutation.id().offset());
+ Preconditions.checkNotNull(entry);
+ entry.visibility = Visibility.VISIBLE;
+ }
+
+ void remove(int offset)
+ {
+ Entry state = statesMap.remove(offset);
+ Preconditions.checkNotNull(state);
+ statesSet.remove(state);
+ }
+
+ boolean collect(Token token, TableId tableId, boolean includePending, Offsets.OffsetReciever into)
+ {
+ SortedSet<Entry> subset = statesSet.subSet(Entry.start(token, true), Entry.end(token, true));
+ return collect(subset, tableId, includePending, into);
+ }
+
+ boolean collect(AbstractBounds<PartitionPosition> range, TableId tableId, boolean includePending, Offsets.OffsetReciever into)
+ {
+ Entry start = Entry.start(range.left.getToken(), range.left.kind() != PartitionPosition.Kind.MAX_BOUND);
+ Entry end = Entry.end(range.right.getToken(), range.right.kind() != PartitionPosition.Kind.MIN_BOUND);
+ return collect(start, end, tableId, includePending, into);
+ }
+
+ private boolean collect(SortedSet<Entry> subset, TableId tableId, boolean includePending, Offsets.OffsetReciever into)
+ {
+ boolean found = false;
+ for (Entry entry : subset)
+ {
+ if (entry.contains(tableId) && (includePending || entry.isVisible()))
+ {
+ into.add(entry.offset);
+ found = true;
+ }
+ }
+ return found;
+ }
+
+ private boolean collect(Entry start, Entry end, TableId tableId, boolean includePending, Offsets.OffsetReciever into)
+ {
+ int cmp = start.token.compareTo(end.token);
+ if (cmp == 0)
+ {
+ // full range
+ return collect(statesSet, tableId, includePending, into);
+ }
+ else if (cmp > 0)
+ {
+ // wrap around range
+ boolean lFound = collect(statesSet.headSet(end), tableId, includePending, into);
+ boolean rFound = collect(statesSet.tailSet(start), tableId, includePending, into);
+ return lFound || rFound;
+ }
+ else
+ {
+ // contiguous range
+ return collect(statesSet.subSet(start, end), tableId, includePending, into);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/replication/Log2OffsetsMap.java b/src/java/org/apache/cassandra/replication/Log2OffsetsMap.java
new file mode 100644
index 0000000000..efcdd195ec
--- /dev/null
+++ b/src/java/org/apache/cassandra/replication/Log2OffsetsMap.java
@@ -0,0 +1,230 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.replication;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterables;
+
+import org.agrona.collections.Long2ObjectHashMap;
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+
+public abstract class Log2OffsetsMap<T extends Offsets> implements Iterable<ShortMutationId>
+{
+ abstract Long2ObjectHashMap<T> offsetMap();
+
+ @Override
+ public Iterator<ShortMutationId> iterator()
+ {
+ return Iterables.concat(offsetMap().values()).iterator();
+ }
+
+ public int idCount()
+ {
+ int count = 0;
+ for (T offsets : offsetMap().values())
+ count += offsets.offsetCount();
+ return count;
+ }
+
+ public boolean isEmpty()
+ {
+ for (T offsets : offsetMap().values())
+ if (!offsets.isEmpty())
+ return false;
+ return true;
+ }
+
+ private static abstract class AbstractMutable<T extends Offsets.AbstractMutable<T>> extends Log2OffsetsMap<T>
+ {
+ protected final Long2ObjectHashMap<T> offsetMap = new Long2ObjectHashMap<>();
+
+ @Override
+ Long2ObjectHashMap<T> offsetMap()
+ {
+ return offsetMap;
+ }
+
+ protected abstract T createOrNull(Offsets.RangeIterator iterator);
+ protected abstract T create(CoordinatorLogId logId);
+ protected abstract T copy(Offsets offsets);
+
+ protected T create(long logId)
+ {
+ return create(new CoordinatorLogId(logId));
+ }
+
+ public void add(ShortMutationId id)
+ {
+ T offsets = offsetMap.computeIfAbsent(id.logId(), this::create);
+ offsets.add(id.offset());
+ }
+
+ public void add(Offsets offsets)
+ {
+ if (offsets.isEmpty())
+ return;
+
+ T existing = offsetMap.get(offsets.logId().asLong());
+ if (existing == null)
+ {
+ offsetMap.put(offsets.logId().asLong(), copy(offsets));
+ return;
+ }
+
+ existing.addAll(offsets);
+ }
+
+ public void addAll(Log2OffsetsMap<?> that)
+ {
+ for (Offsets offsets : that.offsetMap().values())
+ add(offsets);
+ }
+
+ public void remove(Offsets offsets)
+ {
+ T existing = offsetMap.get(offsets.logId().asLong());
+ if (existing == null)
+ return;
+
+ if (existing.isEmpty())
+ {
+ offsetMap.remove(offsets.logId().asLong());
+ return;
+ }
+
+ T next = createOrNull(Offsets.difference(existing.rangeIterator(), offsets.rangeIterator()));
+ if (next == null)
+ offsetMap.remove(offsets.logId().asLong());
+ else
+ offsetMap.put(offsets.logId().asLong(), next);
+ }
+
+ public void removeAll(Log2OffsetsMap<?> that)
+ {
+ for (Offsets offsets : that.offsetMap().values())
+ remove(offsets);
+ }
+ }
+
+ public static class Mutable extends AbstractMutable<Offsets.Mutable>
+ {
+ @Override
+ protected Offsets.Mutable createOrNull(Offsets.RangeIterator iterator)
+ {
+ return Offsets.Mutable.createOrNull(iterator);
+ }
+
+ @Override
+ protected Offsets.Mutable create(CoordinatorLogId logId)
+ {
+ return new Offsets.Mutable(logId);
+ }
+
+ @Override
+ protected Offsets.Mutable copy(Offsets offsets)
+ {
+ return Offsets.Mutable.copy(offsets);
+ }
+ }
+
+ public static class Immutable extends Log2OffsetsMap<Offsets.Immutable>
+ {
+ private final Long2ObjectHashMap<Offsets.Immutable> offsetMap;
+
+ private Immutable(Long2ObjectHashMap<Offsets.Immutable> offsetMap)
+ {
+ this.offsetMap = offsetMap;
+ }
+
+ @Override
+ Long2ObjectHashMap<Offsets.Immutable> offsetMap()
+ {
+ return offsetMap;
+ }
+
+ public static class Builder extends AbstractMutable<Offsets.Immutable.Builder>
+ {
+ @Override
+ protected Offsets.Immutable.Builder createOrNull(Offsets.RangeIterator iterator)
+ {
+ return Offsets.Immutable.Builder.createOrNull(iterator);
+ }
+
+ @Override
+ protected Offsets.Immutable.Builder create(CoordinatorLogId logId)
+ {
+ return new Offsets.Immutable.Builder(logId);
+ }
+
+ @Override
+ protected Offsets.Immutable.Builder copy(Offsets offsets)
+ {
+ return Offsets.Immutable.Builder.copy(offsets);
+ }
+
+ public Log2OffsetsMap.Immutable build()
+ {
+ Long2ObjectHashMap<Offsets.Immutable> result = new Long2ObjectHashMap<>();
+ offsetMap.forEachLong((key, builder) -> result.put(key, builder.build()));
+ return new Immutable(result);
+ }
+ }
+
+ public static final IVersionedSerializer<Log2OffsetsMap.Immutable> serializer = new IVersionedSerializer<Log2OffsetsMap.Immutable>()
+ {
+ @Override
+ public void serialize(Log2OffsetsMap.Immutable mo, DataOutputPlus out, int version) throws IOException
+ {
+ out.writeInt(mo.offsetMap.size());
+ for (Offsets.Immutable offsets : mo.offsetMap().values())
+ Offsets.serializer.serialize(offsets, out, version);
+ }
+
+ @Override
+ public Log2OffsetsMap.Immutable deserialize(DataInputPlus in, int version) throws IOException
+ {
+ Long2ObjectHashMap<Offsets.Immutable> offsetMap = new Long2ObjectHashMap<>();
+ int size = in.readInt();
+ for (int i=0; i<size; i++)
+ {
+ Offsets.Immutable offsets = Offsets.serializer.deserialize(in, version);
+ long key = offsets.logId().asLong();
+ Preconditions.checkState(!offsetMap.containsKey(key));
+ offsetMap.put(key, offsets);
+ }
+ return new Immutable(offsetMap);
+ }
+
+ @Override
+ public long serializedSize(Log2OffsetsMap.Immutable mo, int version)
+ {
+ long size = TypeSizes.INT_SIZE;
+ for (Offsets.Immutable offsets : mo.offsetMap.values())
+ size += Offsets.serializer.serializedSize(offsets, version);
+ return size;
+ }
+ };
+ }
+}
diff --git a/src/java/org/apache/cassandra/replication/MutationId.java b/src/java/org/apache/cassandra/replication/MutationId.java
new file mode 100644
index 0000000000..ea33708833
--- /dev/null
+++ b/src/java/org/apache/cassandra/replication/MutationId.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.replication;
+
+import java.io.IOException;
+import java.util.Comparator;
+
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+
+/**
+ * Full mutation id, with the addition of timestamp component.
+ * <p>
+ * equals() and hashCode() are intentionally not overridden by this class, since log id and offset alone
+ * are meant to uniquely identify a mutation.
+ */
+public class MutationId extends ShortMutationId
+{
+ private static final long NONE_LOG_ID = Long.MIN_VALUE;
+ private static final long NONE_SEQUENCE_ID = Long.MIN_VALUE;
+ private static final int NONE_OFFSET = offset(NONE_SEQUENCE_ID);
+ private static final int NONE_TIMESTAMP = timestamp(NONE_SEQUENCE_ID);
+ private static final MutationId NONE = new MutationId(NONE_LOG_ID, NONE_SEQUENCE_ID);
+
+ /**
+ * 4 byte timestamp. The timestamp is monotonically non-decreasing.
+ * The offset alone is sufficient to identify the entry within a coordinator
+ * log, the timestamp is added for correlation purposes.
+ */
+ protected final int timestamp;
+
+ public MutationId(long logId, long sequenceId)
+ {
+ super(logId, offset(sequenceId));
+ this.timestamp = timestamp(sequenceId);
+ }
+
+ public long sequenceId()
+ {
+ return sequenceId(offset, timestamp);
+ }
+
+ public int timestamp()
+ {
+ return timestamp;
+ }
+
+ public static long sequenceId(int offset, int timestamp)
+ {
+ return ((long) offset << 32) | timestamp;
+ }
+
+ public static int offset(long sequenceId)
+ {
+ return (int) (0xffffffffL & (sequenceId >> 32));
+ }
+
+ public static int timestamp(long sequenceId)
+ {
+ return (int) (0xffffffffL & sequenceId);
+ }
+
+ // FIXME: used in place of figuring out if we should use a mutation id or not
+ public static MutationId fixme()
+ {
+ return none();
+ }
+
+ public static MutationId none()
+ {
+ return NONE;
+ }
+
+ public boolean isNone()
+ {
+ if (this == NONE)
+ return true;
+ return logId() == NONE_LOG_ID && offset() == NONE_OFFSET && timestamp() == NONE_TIMESTAMP;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "MutationId{" + hostId() + ", " + hostLogId() + ", " + offset() + ", " + timestamp() + '}';
+ }
+
+ /**
+ * The comparator is intentionally not overridden by this class, since log id and offset alone
+ * are meant to uniquely identify a mutation, and only offset determines the order within a log.
+ */
+ public static final Comparator<MutationId> comparator = ShortMutationId.comparator::compare;
+
+ public static final IVersionedSerializer<MutationId> serializer = new IVersionedSerializer<>()
+ {
+ @Override
+ public void serialize(MutationId id, DataOutputPlus out, int version) throws IOException
+ {
+ out.writeLong(id.logId());
+ out.writeLong(id.sequenceId());
+ }
+
+ @Override
+ public MutationId deserialize(DataInputPlus in, int version) throws IOException
+ {
+ long logId = in.readLong();
+ long sequenceId = in.readLong();
+ if (logId == NONE_LOG_ID && sequenceId == NONE_SEQUENCE_ID)
+ return none();
+ return new MutationId(logId, sequenceId);
+ }
+
+ @Override
+ public long serializedSize(MutationId id, int version)
+ {
+ return TypeSizes.sizeof(id.logId()) + TypeSizes.sizeof(id.sequenceId());
+ }
+ };
+}
diff --git a/src/java/org/apache/cassandra/service/tracking/MutationJournal.java b/src/java/org/apache/cassandra/replication/MutationJournal.java
similarity index 57%
rename from src/java/org/apache/cassandra/service/tracking/MutationJournal.java
rename to src/java/org/apache/cassandra/replication/MutationJournal.java
index 3f00e4387c..59f87d5389 100644
--- a/src/java/org/apache/cassandra/service/tracking/MutationJournal.java
+++ b/src/java/org/apache/cassandra/replication/MutationJournal.java
@@ -15,38 +15,33 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.cassandra.service.tracking;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.util.Collection;
-import java.util.concurrent.TimeUnit;
-import java.util.zip.Checksum;
-
-import javax.annotation.Nullable;
+package org.apache.cassandra.replication;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
-
+import org.apache.cassandra.config.Config;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.io.util.File;
-import org.apache.cassandra.journal.Journal;
-import org.apache.cassandra.journal.KeySupport;
-import org.apache.cassandra.journal.Params;
-import org.apache.cassandra.journal.RecordConsumer;
-import org.apache.cassandra.journal.RecordPointer;
-import org.apache.cassandra.journal.SegmentCompactor;
-import org.apache.cassandra.journal.ValueSerializer;
+import org.apache.cassandra.journal.*;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.utils.FBUtilities;
+import javax.annotation.Nullable;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+import java.util.zip.Checksum;
+
public class MutationJournal
{
- private final Journal<MutationId, Mutation> journal;
+ public static final MutationJournal instance = new MutationJournal();
+
+ private final Journal<ShortMutationId, Mutation> journal;
private MutationJournal()
{
@@ -69,25 +64,25 @@ public class MutationJournal
journal.shutdown();
}
- public RecordPointer write(MutationId id, Mutation mutation)
+ public RecordPointer write(ShortMutationId id, Mutation mutation)
{
return journal.blockingWrite(id, mutation);
}
@Nullable
- public Mutation read(MutationId id)
+ public Mutation read(ShortMutationId id)
{
return journal.readLast(id);
}
- public boolean read(MutationId id, RecordConsumer<MutationId> consumer)
+ public boolean read(ShortMutationId id, RecordConsumer<ShortMutationId> consumer)
{
return journal.readLast(id, consumer);
}
- public void readAll(Iterable<MutationId> ids, Collection<Mutation> into)
+ public void readAll(Iterable<ShortMutationId> ids, Collection<Mutation> into)
{
- for (MutationId id : ids)
+ for (ShortMutationId id : ids)
{
Mutation mutation = read(id);
Preconditions.checkState(mutation != null);
@@ -112,7 +107,18 @@ public class MutationJournal
@Override
public FlushMode flushMode()
{
- return FlushMode.PERIODIC;
+ Config.CommitLogSync mode = DatabaseDescriptor.getCommitLogSync();
+ switch (DatabaseDescriptor.getCommitLogSync())
+ {
+ case batch:
+ return FlushMode.BATCH;
+ case periodic:
+ return FlushMode.PERIODIC;
+ case group:
+ return FlushMode.GROUP;
+ default:
+ throw new IllegalStateException("Unhandled flush mode: " + mode);
+ }
}
@Override
@@ -146,88 +152,88 @@ public class MutationJournal
}
}
- static class MutationIdSupport implements KeySupport<MutationId>
+ static class MutationIdSupport implements KeySupport<ShortMutationId>
{
static final int LOG_ID_OFFSET = 0;
- static final int SEQUENCE_ID_OFFSET = LOG_ID_OFFSET + TypeSizes.LONG_SIZE;
+ static final int OFFSET_OFFSET = LOG_ID_OFFSET + TypeSizes.LONG_SIZE;
@Override
public int serializedSize(int userVersion)
{
return TypeSizes.LONG_SIZE // logId
- + TypeSizes.LONG_SIZE; // sequenceId
+ + TypeSizes.INT_SIZE; // offset
}
@Override
- public void serialize(MutationId id, DataOutputPlus out, int userVersion) throws IOException
+ public void serialize(ShortMutationId id, DataOutputPlus out, int userVersion) throws IOException
{
- out.writeLong(id.logId);
- out.writeLong(id.sequenceId);
+ out.writeLong(id.logId());
+ out.writeInt(id.offset());
}
@Override
- public void serialize(MutationId id, ByteBuffer out, int userVersion) throws IOException
+ public void serialize(ShortMutationId id, ByteBuffer out, int userVersion) throws IOException
{
- out.putLong(id.logId);
- out.putLong(id.sequenceId);
+ out.putLong(id.logId());
+ out.putInt(id.offset());
}
@Override
- public MutationId deserialize(DataInputPlus in, int userVersion) throws IOException
+ public ShortMutationId deserialize(DataInputPlus in, int userVersion) throws IOException
{
long logId = in.readLong();
- long sequenceId = in.readLong();
- return new MutationId(logId, sequenceId);
+ int offset = in.readInt();
+ return new ShortMutationId(logId, offset);
}
@Override
- public MutationId deserialize(ByteBuffer buffer, int position, int userVersion)
+ public ShortMutationId deserialize(ByteBuffer buffer, int position, int userVersion)
{
long logId = buffer.getLong(position + LOG_ID_OFFSET);
- long sequenceId = buffer.getLong(position + SEQUENCE_ID_OFFSET);
- return new MutationId(logId, sequenceId);
+ int offset = buffer.getInt(position + OFFSET_OFFSET);
+ return new ShortMutationId(logId, offset);
}
@Override
- public MutationId deserialize(ByteBuffer buffer, int userVersion)
+ public ShortMutationId deserialize(ByteBuffer buffer, int userVersion)
{
long logId = buffer.getLong();
- long sequenceId = buffer.getLong();
- return new MutationId(logId, sequenceId);
+ int offset = buffer.getInt();
+ return new ShortMutationId(logId, offset);
}
@Override
- public void updateChecksum(Checksum crc, MutationId id, int userVersion)
+ public void updateChecksum(Checksum crc, ShortMutationId id, int userVersion)
{
- FBUtilities.updateChecksumLong(crc, id.logId);
- FBUtilities.updateChecksumLong(crc, id.sequenceId);
+ FBUtilities.updateChecksumLong(crc, id.logId());
+ FBUtilities.updateChecksumInt(crc, id.offset());
}
@Override
- public int compareWithKeyAt(MutationId id, ByteBuffer buffer, int position, int userVersion)
+ public int compareWithKeyAt(ShortMutationId id, ByteBuffer buffer, int position, int userVersion)
{
- int cmp = Long.compare(id.logId, buffer.getLong(position + LOG_ID_OFFSET));
- return cmp != 0 ? cmp : Long.compare(id.sequenceId, buffer.getLong(position + SEQUENCE_ID_OFFSET));
+ int cmp = Long.compare(id.logId(), buffer.getLong(position + LOG_ID_OFFSET));
+ return cmp != 0 ? cmp : Integer.compare(id.offset(), buffer.getInt(position + OFFSET_OFFSET));
}
@Override
- public int compare(MutationId id1, MutationId id2)
+ public int compare(ShortMutationId id1, ShortMutationId id2)
{
- int cmp = Long.compare(id1.logId, id2.logId);
- return cmp != 0 ? cmp : Long.compare(id1.sequenceId, id2.sequenceId);
+ int cmp = Long.compare(id1.logId(), id2.logId());
+ return cmp != 0 ? cmp : Integer.compare(id1.offset(), id2.offset());
}
}
- static class MutationSerializer implements ValueSerializer<MutationId, Mutation>
+ static class MutationSerializer implements ValueSerializer<ShortMutationId, Mutation>
{
@Override
- public void serialize(MutationId id, Mutation mutation, DataOutputPlus out, int userVersion) throws IOException
+ public void serialize(ShortMutationId id, Mutation mutation, DataOutputPlus out, int userVersion) throws IOException
{
Mutation.serializer.serialize(mutation, out, userVersion);
}
@Override
- public Mutation deserialize(MutationId id, DataInputPlus in, int userVersion) throws IOException
+ public Mutation deserialize(ShortMutationId id, DataInputPlus in, int userVersion) throws IOException
{
return Mutation.serializer.deserialize(in, userVersion);
}
diff --git a/src/java/org/apache/cassandra/replication/MutationSummary.java b/src/java/org/apache/cassandra/replication/MutationSummary.java
new file mode 100644
index 0000000000..3483f710ec
--- /dev/null
+++ b/src/java/org/apache/cassandra/replication/MutationSummary.java
@@ -0,0 +1,351 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.replication;
+
+import java.io.IOException;
+import java.util.*;
+
+import com.google.common.base.Preconditions;
+import org.agrona.collections.Long2ObjectHashMap;
+import org.apache.cassandra.db.Digest;
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.schema.TableId;
+
+public class MutationSummary
+{
+ public static class CoordinatorSummary
+ {
+ private static final Comparator<CoordinatorSummary> idComparator =
+ (l, r) -> CoordinatorLogId.comparator.compare(l.logId(), r.logId());
+
+ public final Offsets.Immutable reconciled;
+ public final Offsets.Immutable unreconciled;
+
+ public CoordinatorSummary(Offsets.Immutable reconciled, Offsets.Immutable unreconciled)
+ {
+ Preconditions.checkArgument(reconciled.logId().equals(unreconciled.logId()));
+ this.reconciled = reconciled;
+ this.unreconciled = unreconciled;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (o == null || getClass() != o.getClass()) return false;
+ CoordinatorSummary summary = (CoordinatorSummary) o;
+ return reconciled.equals(summary.reconciled) && unreconciled.equals(summary.unreconciled);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(reconciled, unreconciled);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "CoordinatorSummary{" +
+ "logId=" + logId() +
+ ", reconciled=" + reconciled +
+ ", unreconciled=" + unreconciled +
+ '}';
+ }
+
+ public CoordinatorLogId logId()
+ {
+ return reconciled.logId();
+ }
+
+ boolean contains(int offset)
+ {
+ return reconciled.contains(offset) || unreconciled.contains(offset);
+ }
+
+ /**
+ * Finds all elements that are contained by {@code left} and not contained by {@code right}
+ */
+ static void difference(CoordinatorSummary left, CoordinatorSummary right, Collection<ShortMutationId> into)
+ {
+ Offsets.RangeIterator leftIds = Offsets.union(left.reconciled.rangeIterator(), left.unreconciled.rangeIterator());
+ Offsets.RangeIterator rightIds = Offsets.union(right.reconciled.rangeIterator(), right.unreconciled.rangeIterator());
+ Offsets.RangeIterator missing = Offsets.difference(leftIds, rightIds);
+ Offsets.forEachOffset(missing, (logId, offset) -> into.add(new ShortMutationId(logId, offset)));
+ }
+
+ void digest(Digest digest)
+ {
+ reconciled.digest(digest);
+ unreconciled.digest(digest);
+ }
+
+ public static class Builder
+ {
+ public final CoordinatorLogId logId;
+ public final Offsets.Immutable.Builder reconciled;
+ public final Offsets.Immutable.Builder unreconciled;
+
+ public Builder(CoordinatorLogId logId)
+ {
+ this.logId = logId;
+ reconciled = new Offsets.Immutable.Builder(logId);
+ unreconciled = new Offsets.Immutable.Builder(logId);
+ }
+
+ boolean isEmpty()
+ {
+ return reconciled.isEmpty() && unreconciled.isEmpty();
+ }
+
+ public CoordinatorSummary build()
+ {
+ return new CoordinatorSummary(reconciled.build(), unreconciled.build());
+ }
+ }
+
+ public static final IVersionedSerializer<CoordinatorSummary> serializer = new IVersionedSerializer<>()
+ {
+ @Override
+ public void serialize(CoordinatorSummary t, DataOutputPlus out, int version) throws IOException
+ {
+ Offsets.serializer.serialize(t.reconciled, out, version);
+ Offsets.serializer.serialize(t.unreconciled, out, version);
+ }
+
+ @Override
+ public CoordinatorSummary deserialize(DataInputPlus in, int version) throws IOException
+ {
+ return new CoordinatorSummary(Offsets.serializer.deserialize(in, version),
+ Offsets.serializer.deserialize(in, version));
+ }
+
+ @Override
+ public long serializedSize(CoordinatorSummary t, int version)
+ {
+ return Offsets.serializer.serializedSize(t.reconciled, version)
+ + Offsets.serializer.serializedSize(t.unreconciled, version);
+ }
+ };
+ }
+
+ public static class Builder
+ {
+ public final TableId tableId;
+ private final Long2ObjectHashMap<CoordinatorSummary.Builder> builders = new Long2ObjectHashMap<>();
+
+ public Builder(TableId tableId)
+ {
+ this.tableId = tableId;
+ }
+
+ public CoordinatorSummary.Builder builderForLog(CoordinatorLogId logId)
+ {
+ CoordinatorSummary.Builder builder = builders.get(logId.asLong());
+ if (builder == null)
+ {
+ builder = new CoordinatorSummary.Builder(logId);
+ builders.put(logId.asLong(), builder);
+ }
+
+ return builder;
+ }
+
+ public MutationSummary build()
+ {
+ List<CoordinatorSummary> summaries = new ArrayList<>(builders.size());
+ for (CoordinatorSummary.Builder builder : builders.values())
+ if (!builder.isEmpty())
+ summaries.add(builder.build());
+
+ summaries.sort(CoordinatorSummary.idComparator);
+ return new MutationSummary(tableId, summaries);
+ }
+ }
+
+ private final TableId tableId;
+ private final List<CoordinatorSummary> summaries;
+ private transient final Long2ObjectHashMap<CoordinatorSummary> coordinatorSummaryMap = new Long2ObjectHashMap<>();
+
+ private MutationSummary(TableId tableId, List<CoordinatorSummary> summaries)
+ {
+ long lastId = 0;
+ for (int i=0, mi=summaries.size(); i<mi; i++)
+ {
+ CoordinatorSummary summary = summaries.get(i);
+ long thisId = summary.logId().asLong();
+ if (i > 0 && thisId <= lastId)
+ throw new IllegalArgumentException("duplicated or unsorted log id found");
+
+ coordinatorSummaryMap.put(thisId, summary);
+ lastId = thisId;
+ }
+
+ this.tableId = tableId;
+ this.summaries = summaries;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (o == null || getClass() != o.getClass()) return false;
+ MutationSummary summary = (MutationSummary) o;
+ return tableId.equals(summary.tableId) && summaries.equals(summary.summaries);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return tableId.hashCode() + 31 * summaries.hashCode();
+ }
+
+ @Override
+ public String toString()
+ {
+ return "MutationSummary{tableId=" + tableId + ", summaries=" + summaries + '}';
+ }
+
+ public TableId tableId()
+ {
+ return tableId;
+ }
+
+ public byte[] digest()
+ {
+ Digest digest = Digest.forReadResponse();
+ digest.updateWithLong(tableId.asUUID().getMostSignificantBits());
+ digest.updateWithLong(tableId.asUUID().getLeastSignificantBits());
+ digest.updateWithInt(summaries.size());
+
+ for (CoordinatorSummary summary : summaries)
+ summary.digest(digest);
+
+ return digest.digest();
+ }
+
+ public boolean contains(ShortMutationId id)
+ {
+ CoordinatorSummary summary = coordinatorSummaryMap.get(id.logId());
+ return summary != null && summary.contains(id.offset());
+ }
+
+ public int unreconciledIds()
+ {
+ int count = 0;
+ for (CoordinatorSummary summary : summaries)
+ count += summary.unreconciled.offsetCount();
+ return count;
+ }
+
+ public int size()
+ {
+ return summaries.size();
+ }
+
+ boolean isEmpty()
+ {
+ return size() == 0;
+ }
+
+ public CoordinatorSummary get(int i)
+ {
+ return summaries.get(i);
+ }
+
+ public CoordinatorSummary get(CoordinatorLogId logId)
+ {
+ return coordinatorSummaryMap.get(logId.asLong());
+ }
+
+ /**
+ * Finds all elements that are contained by {@code left} and not contained by {@code right}
+ */
+ public static void difference(MutationSummary left, MutationSummary right, Collection<ShortMutationId> into)
+ {
+ int i = 0, j = 0, lsize = left.size(), rsize = right.size();
+
+ while (i < lsize && j < rsize)
+ {
+ CoordinatorSummary l = left.get(i);
+ CoordinatorSummary r = right.get(j);
+
+ int cmp = CoordinatorSummary.idComparator.compare(l, r);
+
+ if (cmp == 0)
+ {
+ CoordinatorSummary.difference(l, r, into);
+ ++i;
+ ++j;
+ }
+ else if (cmp < 0)
+ {
+ l.reconciled.collectIds(into);
+ l.unreconciled.collectIds(into);
+ ++i;
+ }
+ else
+ {
+ ++j;
+ }
+ }
+
+ while (i < lsize)
+ {
+ CoordinatorSummary l = left.get(i);
+ l.reconciled.collectIds(into);
+ l.unreconciled.collectIds(into);
+ ++i;
+ }
+ }
+
+ public static final IVersionedSerializer<MutationSummary> serializer = new IVersionedSerializer<>()
+ {
+ @Override
+ public void serialize(MutationSummary summary, DataOutputPlus out, int version) throws IOException
+ {
+ summary.tableId.serialize(out);
+ out.writeInt(summary.summaries.size());
+ for (int i=0,mi=summary.summaries.size(); i<mi; i++)
+ CoordinatorSummary.serializer.serialize(summary.summaries.get(i), out, version);
+ }
+
+ @Override
+ public MutationSummary deserialize(DataInputPlus in, int version) throws IOException
+ {
+ TableId tableId = TableId.deserialize(in);
+ int size = in.readInt();
+ List<CoordinatorSummary> summaries = new ArrayList<>(size);
+ for (int i = 0; i < size; i++)
+ summaries.add(CoordinatorSummary.serializer.deserialize(in, version));
+
+ return new MutationSummary(tableId, summaries);
+ }
+
+ @Override
+ public long serializedSize(MutationSummary summary, int version)
+ {
+ long size = summary.tableId.serializedSize();
+ size += TypeSizes.sizeof(summary.summaries.size());
+ for (int i=0,mi=summary.summaries.size(); i<mi; i++)
+ size += CoordinatorSummary.serializer.serializedSize(summary.summaries.get(i), version);
+ return size;
+ }
+ };
+}
diff --git a/src/java/org/apache/cassandra/replication/MutationTrackingService.java b/src/java/org/apache/cassandra/replication/MutationTrackingService.java
new file mode 100644
index 0000000000..0c48c3e498
--- /dev/null
+++ b/src/java/org/apache/cassandra/replication/MutationTrackingService.java
@@ -0,0 +1,239 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.replication;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import java.util.function.IntSupplier;
+
+import org.agrona.collections.IntArrayList;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.PartitionPosition;
+import org.apache.cassandra.dht.AbstractBounds;
+import org.apache.cassandra.dht.Range;
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.locator.InetAddressAndPort;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.Schema;
+import org.apache.cassandra.schema.TableId;
+import org.apache.cassandra.service.reads.tracked.TrackedLocalReads;
+import org.apache.cassandra.tcm.ClusterMetadata;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+// TODO (expected): persistence (handle restarts)
+// TODO (expected): handle topology changes
+public class MutationTrackingService
+{
+ private static final Logger logger = LoggerFactory.getLogger(MutationTrackingService.class);
+ public static final MutationTrackingService instance = new MutationTrackingService();
+
+ private final TrackedLocalReads localReads = new TrackedLocalReads();
+ private final ConcurrentHashMap<String, KeyspaceShards> shards = new ConcurrentHashMap<>();
+
+ private volatile boolean started = false;
+
+ private MutationTrackingService() {}
+
+ // TODO (expected): implement a TCM ChangeListener
+ public synchronized void start(ClusterMetadata metadata)
+ {
+ if (started)
+ return;
+
+ logger.info("Starting replication tracking service");
+
+ for (KeyspaceMetadata keyspace : metadata.schema.getKeyspaces())
+ if (keyspace.useMutationTracking())
+ shards.put(keyspace.name, KeyspaceShards.make(keyspace, metadata, this::nextHostLogId));
+ started = true;
+ }
+
+ public synchronized boolean isStarted()
+ {
+ return started;
+ }
+
+ public void shutdownBlocking() throws InterruptedException
+ {
+ localReads.shutdownBlocking();
+ }
+
+ public TrackedLocalReads localReads()
+ {
+ return localReads;
+ }
+
+ public MutationId nextMutationId(String keyspace, Token token)
+ {
+ MutationId id = getOrCreate(keyspace).nextMutationId(token);
+ logger.trace("Created new mutation id {}", id);
+ return id;
+ }
+
+ public void witnessedRemoteMutation(String keyspace, Token token, MutationId mutationId, InetAddressAndPort onHost)
+ {
+ getOrCreate(keyspace).witnessedRemoteMutation(token, mutationId, onHost);
+ }
+
+ public void startWriting(Mutation mutation)
+ {
+ getOrCreate(mutation.getKeyspaceName()).startWriting(mutation);
+ }
+
+ public void finishWriting(Mutation mutation)
+ {
+ getOrCreate(mutation.getKeyspaceName()).finishWriting(mutation);
+ }
+
+ public MutationSummary createSummaryForKey(DecoratedKey key, TableId tableId, boolean includePending)
+ {
+ return getOrCreate(tableId).createSummaryForKey(key, tableId, includePending);
+ }
+
+ public MutationSummary createSummaryForRange(AbstractBounds<PartitionPosition> range, TableId tableId, boolean includePending)
+ {
+ return getOrCreate(tableId).createSummaryForRange(range, tableId, includePending);
+ }
+
+ public MutationSummary createSummaryForRange(Range<Token> range, TableId tableId, boolean includePending)
+ {
+ return createSummaryForRange(Range.makeRowRange(range), tableId, includePending);
+ }
+
+ private KeyspaceShards getOrCreate(TableId tableId)
+ {
+ //noinspection DataFlowIssue
+ return getOrCreate(Schema.instance.getTableMetadata(tableId).keyspace);
+ }
+
+ private KeyspaceShards getOrCreate(String keyspace)
+ {
+ KeyspaceShards ks = shards.get(keyspace);
+ if (ks != null)
+ return ks;
+
+ ClusterMetadata csm = ClusterMetadata.current();
+ KeyspaceMetadata ksm = csm.schema.getKeyspaceMetadata(keyspace);
+ return shards.computeIfAbsent(keyspace, ignore -> KeyspaceShards.make(ksm, csm, this::nextHostLogId));
+ }
+
+ // TODO (expected): durability
+ int nextHostLogId()
+ {
+ return nextHostLogId.incrementAndGet();
+ }
+ private final AtomicInteger nextHostLogId = new AtomicInteger();
+
+ private static class KeyspaceShards
+ {
+ private final String keyspace;
+ private final Map<Range<Token>, Shard> shards;
+
+ private transient final Map<Range<PartitionPosition>, Shard> ppShards;
+
+ static KeyspaceShards make(KeyspaceMetadata keyspace, ClusterMetadata cluster, IntSupplier logIdProvider)
+ {
+ Map<Range<Token>, Shard> shards = new HashMap<>();
+ cluster.placements.get(keyspace.params.replication).writes.forEach((tokenRange, forRange) -> {
+ IntArrayList participants = new IntArrayList(forRange.size(), IntArrayList.DEFAULT_NULL_VALUE);
+ for (InetAddressAndPort endpoint : forRange.endpoints())
+ participants.add(cluster.directory.peerId(endpoint).id());
+ Shard shard = new Shard(keyspace.name, tokenRange, cluster.myNodeId().id(), new Participants(participants), forRange.lastModified(), logIdProvider);
+ shards.put(tokenRange, shard);
+ });
+ return new KeyspaceShards(keyspace.name, shards);
+ }
+
+ KeyspaceShards(String keyspace, Map<Range<Token>, Shard> shards)
+ {
+ this.keyspace = keyspace;
+ this.shards = shards;
+
+ this.ppShards = new HashMap<>();
+ shards.forEach((range, shard) -> ppShards.put(Range.makeRowRange(range), shard));
+ }
+
+ MutationId nextMutationId(Token token)
+ {
+ return lookUp(token).nextId();
+ }
+
+ void witnessedRemoteMutation(Token token, MutationId mutationId, InetAddressAndPort onHost)
+ {
+ lookUp(token).witnessedRemoteMutation(mutationId, onHost);
+ }
+
+ void startWriting(Mutation mutation)
+ {
+ lookUp(mutation).startWriting(mutation);
+ }
+
+ void finishWriting(Mutation mutation)
+ {
+ lookUp(mutation).finishWriting(mutation);
+ }
+
+ MutationSummary createSummaryForKey(DecoratedKey key, TableId tableId, boolean includePending)
+ {
+ MutationSummary.Builder builder = new MutationSummary.Builder(tableId);
+ lookUp(key.getToken()).addSummaryForKey(key.getToken(), includePending, builder);
+ return builder.build();
+ }
+
+ MutationSummary createSummaryForRange(AbstractBounds<PartitionPosition> range, TableId tableId, boolean includePending)
+ {
+ MutationSummary.Builder builder = new MutationSummary.Builder(tableId);
+ forEachIntersectingShard(range, shard -> shard.addSummaryForRange(range, includePending, builder));
+ return builder.build();
+ }
+
+ private void forEachIntersectingShard(AbstractBounds<PartitionPosition> bounds, Consumer<Shard> consumer)
+ {
+ ppShards.forEach((range, shard) -> {
+ // TODO (expected): partial workaround - is there a better way to do this?
+ // SELECT * statements create Bounds[min,min], (PartitionKeyRestrictions.java:L174) not Range(min,min],
+ // which Ranges generally won't intersect with (Range.java:L148), so contains is used here to make it work
+ if (bounds.contains(range.right) || range.intersects(bounds))
+ consumer.accept(shard);
+ });
+ }
+
+ Shard lookUp(Mutation mutation)
+ {
+ return lookUp(mutation.key());
+ }
+
+ Shard lookUp(DecoratedKey key)
+ {
+ return lookUp(key.getToken());
+ }
+
+ Shard lookUp(Token token)
+ {
+ ClusterMetadata csm = ClusterMetadata.current();
+ KeyspaceMetadata ksm = csm.schema.getKeyspaceMetadata(keyspace);
+ Range<Token> range = ClusterMetadata.current().placements.get(ksm.params.replication).writes.forRange(token).range();
+ return shards.get(range);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/replication/Offsets.java b/src/java/org/apache/cassandra/replication/Offsets.java
new file mode 100644
index 0000000000..df1512617a
--- /dev/null
+++ b/src/java/org/apache/cassandra/replication/Offsets.java
@@ -0,0 +1,1496 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.replication;
+
+import com.google.common.base.Function;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+
+import org.apache.cassandra.db.Digest;
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.utils.AbstractIterator;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+
+public abstract class Offsets implements Iterable<ShortMutationId>
+{
+ private static final int INITIAL_CAPACITY = 16;
+
+ protected final CoordinatorLogId logId;
+ // even index is range start, odd index is range end (inclusive)
+ protected int[] bounds;
+ protected int size;
+
+ protected Offsets(CoordinatorLogId logId, int[] bounds)
+ {
+ this(logId, bounds, bounds.length);
+ }
+
+ protected Offsets(CoordinatorLogId logId, int[] bounds, int size)
+ {
+ this.logId = logId;
+ this.bounds = bounds;
+ this.size = size;
+ }
+
+ public static boolean contentsEqual(Offsets a, Offsets b)
+ {
+ if (a == null || b == null)
+ return a == null && b == null;
+ if (a.size != b.size)
+ return false;
+ if (!a.logId.equals(b.logId))
+ return false;
+ return Arrays.equals(a.bounds, 0, a.size, b.bounds, 0, b.size);
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (o == null || getClass() != o.getClass()) return false;
+ Offsets that = (Offsets) o;
+ return size == that.size && Objects.equal(logId, that.logId) && Arrays.equals(bounds, 0, size, that.bounds, 0, size);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ int result = logId.hashCode();
+ result = 31 * result + Integer.hashCode(size);
+ for (int i = 0; i < size; i++)
+ result = 31 * result + Integer.hashCode(bounds[i]);
+ return result;
+ }
+
+ @Override
+ public Iterator<ShortMutationId> iterator()
+ {
+ return new AbstractIterator<>()
+ {
+ int range = -1;
+ int lastOffset = Integer.MAX_VALUE;
+
+ @Override
+ protected ShortMutationId computeNext()
+ {
+ if (range < 0 || lastOffset >= bounds[rangeEnd(range)])
+ {
+ range++;
+ if (range >= rangeCount())
+ return endOfData();
+
+ lastOffset = bounds[rangeStart(range)];
+ }
+ else
+ {
+ lastOffset++;
+ }
+
+ return new ShortMutationId(logId, lastOffset);
+ }
+ };
+ }
+
+ public CoordinatorLogId logId()
+ {
+ return logId;
+ }
+
+ public int rangeCount()
+ {
+ return size / 2;
+ }
+
+ public int offsetCount()
+ {
+ int count = 0, i = 0;
+ while (i < size)
+ {
+ int start = bounds[i++];
+ int end = bounds[i++];
+ count += end - start + 1;
+ }
+ return count;
+ }
+
+ public boolean isEmpty()
+ {
+ return size == 0;
+ }
+
+ private static void forEachOffsetInRange(CoordinatorLogId logId, int start, int end, OffsetConsumer consumer)
+ {
+ for (int offset = start; offset <= end; offset++)
+ consumer.accept(logId, offset);
+ }
+
+ public static void forEachOffset(RangeIterator iter, OffsetConsumer consumer)
+ {
+ while (iter.tryAdvance())
+ forEachOffsetInRange(iter.logId(), iter.start(), iter.end(), consumer);
+ }
+
+ public void collectIds(Collection<ShortMutationId> into)
+ {
+ for (int i = 0; i < size; i += 2)
+ {
+ int start = bounds[i];
+ int end = bounds[i + 1];
+ for (int offset = start; offset <= end; offset++)
+ into.add(new ShortMutationId(logId, offset));
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder builder = new StringBuilder("{");
+ int i = 0;
+ while (i < size)
+ {
+ int start = bounds[i++];
+ int end = bounds[i++];
+ builder.append('[').append(start).append(',').append(end).append(']');
+ if (i < size) builder.append(',');
+ }
+ return builder.append('}').toString();
+ }
+
+ public boolean contains(int offset)
+ {
+ if (size == 0)
+ return false;
+
+ int pos = Arrays.binarySearch(bounds, 0, size, offset);
+ if (pos >= 0) return true; // matches one of the bounds
+
+ pos = -pos - 1;
+ return (pos - 1) % 2 == 0; // offset falls within bounds of an existing range if the bound to the left is an open one
+ }
+
+ public void digest(Digest digest)
+ {
+ digest.updateWithLong(logId.asLong());
+ digest.updateWithInt(size);
+ for (int i = 0; i < size; i++)
+ digest.updateWithInt(bounds[i]);
+ }
+
+ static abstract class AbstractMutable<T extends AbstractMutable<T>> extends Offsets implements OffsetReciever
+ {
+ AbstractMutable(RangeIterator rangeIterator)
+ {
+ this(rangeIterator.logId());
+ while (rangeIterator.tryAdvance())
+ append(rangeIterator.start(), rangeIterator.end());
+ }
+
+ AbstractMutable(CoordinatorLogId logId)
+ {
+ this(logId, INITIAL_CAPACITY);
+ }
+
+ AbstractMutable(CoordinatorLogId logId, int capacity)
+ {
+ super(logId, new int[capacity], 0);
+ }
+ AbstractMutable(CoordinatorLogId logId, int[] bounds)
+ {
+ this(logId, bounds, bounds.length);
+ }
+
+ AbstractMutable(CoordinatorLogId logId, int[] bounds, int size)
+ {
+ super(logId, bounds, size);
+ }
+
+ protected static <T extends AbstractMutable<T>> T createOrNull(Offsets.RangeIterator rangeIterator, Function<CoordinatorLogId, T> ctor)
+ {
+ T result = null;
+
+ while (rangeIterator.tryAdvance())
+ {
+ if (result == null)
+ result = ctor.apply(rangeIterator.logId());
+ result.append(rangeIterator.start(), rangeIterator.end());
+ }
+
+ return result;
+ }
+
+ public void addAll(Offsets other, RangeConsumer onAdded)
+ {
+ for (int i = 0; i < other.size; i += 2)
+ add(other.bounds[i], other.bounds[i + 1], onAdded);
+ }
+
+ public void addAll(Offsets other)
+ {
+ addAll(other, RangeConsumer.NONE);
+ }
+
+ public boolean add(int offset, RangeConsumer onAdded)
+ {
+ boolean added = add(offset);
+ if (added) onAdded.consume(logId, offset, offset);
+ return added;
+ }
+
+ public boolean add(int offset)
+ {
+ if (size == 0)
+ {
+ append(offset, offset);
+ return true;
+ }
+
+ int pos = Arrays.binarySearch(bounds, 0, size, offset);
+ if (pos >= 0) return false; // matches one of the bounds
+
+ pos = -pos - 1;
+ if (pos == size) // after all existing ranges
+ {
+ if (bounds[size - 1] == offset - 1)
+ bounds[size - 1] = offset; // extend the last range
+ else
+ append(offset, offset); // append a new single-offset range
+
+ return true;
+ }
+ else if (pos == 0) // before all existing ranges
+ {
+ if (bounds[0] == offset + 1)
+ bounds[0] = offset; // extend the first range
+ else
+ insert(0, offset, offset); // prepend a new single-offset range
+
+ return true;
+ }
+ else if ((pos - 1) % 2 == 0) // offset falls within bounds of an existing range (bound to the left is an open bound)
+ {
+ return false;
+ }
+
+ // between two existing ranges
+ boolean extendsPrev = bounds[pos - 1] == offset - 1;
+ boolean extendsNext = bounds[pos] == offset + 1;
+
+ if (extendsPrev && extendsNext) // closes the gap between two adjacent ranges
+ {
+ bounds[pos - 1] = bounds[pos + 1];
+ System.arraycopy(bounds, pos + 2, bounds, pos, size - pos - 2);
+ bounds[--size] = 0;
+ bounds[--size] = 0;
+ }
+ else if (extendsPrev)
+ {
+ bounds[pos - 1] = offset;
+ }
+ else if (extendsNext)
+ {
+ bounds[pos] = offset;
+ }
+ else
+ {
+ insert(pos, offset, offset);
+ }
+
+ return true;
+ }
+
+ private enum AddAction
+ {
+ INSERT, MOVE, INCLUDE;
+
+ boolean isMove()
+ {
+ return this == MOVE;
+ }
+
+ boolean isInclude()
+ {
+ return this == INCLUDE;
+ }
+
+ boolean isInsert()
+ {
+ return this == INSERT;
+ }
+
+ boolean isMoveOrInclude()
+ {
+ return this == MOVE || this == INCLUDE;
+ }
+ }
+
+ public boolean add(final int start, final int end, RangeConsumer onAdded)
+ {
+ Preconditions.checkArgument(start <= end);
+
+ if (size == 0)
+ {
+ append(start, end);
+ return true;
+ }
+
+ if (start == end)
+ return add(start, onAdded);
+
+ int spos = Arrays.binarySearch(bounds, 0, size, start);
+ int epos = Arrays.binarySearch(bounds, 0, size, end);
+
+ if (spos >= 0 && spos % 2 == 0 && epos == spos + 1) return false; // matches an existing bound
+
+ if (spos < 0) spos = -spos - 1;
+ if (epos < 0) epos = -epos - 1;
+
+ int numRanges = rangeCount();
+ int sRange = Math.min(spos/2, numRanges - 1);
+ int eRange = Math.min(epos/2, numRanges - 1);
+
+ AddAction sMerge;
+ {
+ int rStart = bounds[rangeStart(sRange)];
+ int rEnd = bounds[rangeEnd(sRange)];
+ if (start >= rStart)
+ {
+ // already included in the range or adjacent to range end
+ sMerge = start <= rEnd + 1
+ ? AddAction.INCLUDE // included in the range
+ : AddAction.INSERT; // past the end of the range
+ }
+ else if (sRange > 0 && start == bounds[rangeEnd(sRange - 1)] + 1)
+ {
+ // adjacent to the previous range, so say we're included in it to merge
+ sRange--;
+ sMerge = AddAction.INCLUDE;
+ }
+ else
+ {
+ sMerge = AddAction.MOVE;
+ }
+ }
+
+ AddAction eMerge;
+ {
+ int rStart = bounds[rangeStart(eRange)];
+ int rEnd = bounds[rangeEnd(eRange)];
+
+ if (end <= rEnd)
+ {
+ if (end >= rStart - 1)
+ {
+ // included in the range or adjacent to range start
+ eMerge = AddAction.INCLUDE;
+ }
+ else if (sRange == eRange - 1)
+ {
+ // if we're before the start of this range, and the start is assigned to
+ // the previous range, then we should just extend the previous range
+ eRange--;
+ eMerge = AddAction.MOVE;
+ }
+ else
+ {
+ // before the start of the range
+ eMerge = AddAction.INSERT;
+ }
+ }
+ else if (eRange < numRanges - 1 && end == bounds[rangeStart(eRange + 1)] - 1)
+ {
+ // adjacent to the next range, so say we're included in it to merge
+ eRange++;
+ eMerge = AddAction.INCLUDE;
+ }
+
+ else
+ {
+ eMerge = AddAction.MOVE;
+ }
+ }
+
+ // this range isn't adjacent and doesn't intersect any existing, so create a new range
+ if (sMerge.isMove() && eMerge.isInsert())
+ {
+ Preconditions.checkState(sRange == eRange);
+ onAdded.consume(logId, start, end);
+ insert(rangeStart(sRange), start, end);
+ return true;
+ }
+
+ // this should only happen if we're adding a range to the very end of the set
+ if (sMerge.isInsert() && eMerge.isMove())
+ {
+ Preconditions.checkState(sRange == eRange);
+ Preconditions.checkState(sRange == numRanges - 1);
+ onAdded.consume(logId, start, end);
+ append(start, end);
+ return true;
+ }
+
+ boolean adjusted = false;
+ if (sMerge.isMove())
+ {
+ onAdded.consume(logId, start, bounds[rangeStart(sRange)] - 1);
+ bounds[rangeStart(sRange)] = start;
+ adjusted = true;
+ }
+
+ // combine existing ranges
+ if (sRange != eRange)
+ {
+ Preconditions.checkState(sMerge.isMoveOrInclude());
+ Preconditions.checkState(eMerge.isMoveOrInclude());
+
+ adjusted = true;
+ // report merged ranges
+ for (int i = sRange; i < eRange; i++)
+ {
+ int sEnd = bounds[rangeEnd(i)];
+ int eStart = bounds[rangeStart(i + 1)];
+ onAdded.consume(logId, sEnd + 1, eStart - 1);
+ }
+
+ // move array back -
+ int dstIdx = rangeEnd(sRange);
+ int srcIdx = rangeEnd(eRange);
+ System.arraycopy(bounds, srcIdx, bounds, dstIdx, size - srcIdx);
+ while (eRange > sRange)
+ {
+ eRange--;
+ bounds[--size] = 0;
+ bounds[--size] = 0;
+ }
+ }
+
+ if (eMerge.isMove())
+ {
+ onAdded.consume(logId, bounds[rangeEnd(eRange)] + 1, end);
+ bounds[rangeEnd(eRange)] = end;
+ adjusted = true;
+ }
+
+ return adjusted;
+ }
+
+ public boolean add(int start, int end)
+ {
+ return add(start, end, RangeConsumer.NONE);
+ }
+
+ private void insert(int pos, int start, int end)
+ {
+ if (bounds.length == size)
+ {
+ int[] newBounds = new int[bounds.length * 2];
+ System.arraycopy(bounds, 0, newBounds, 0, pos);
+ System.arraycopy(bounds, pos, newBounds, pos + 2, size - pos);
+ bounds = newBounds;
+ }
+ else
+ {
+ System.arraycopy(bounds, pos, bounds, pos + 2, size - pos);
+ }
+ bounds[pos] = start;
+ bounds[pos + 1] = end;
+ size += 2;
+ }
+
+ protected void append(int start, int end)
+ {
+ if (bounds.length == size)
+ {
+ int[] newBounds = new int[Math.max(bounds.length * 2, INITIAL_CAPACITY)];
+ System.arraycopy(bounds, 0, newBounds, 0, bounds.length);
+ bounds = newBounds;
+ }
+ Preconditions.checkState(size == 0 || start > bounds[size - 1]);
+ bounds[size++] = start;
+ bounds[size++] = end;
+ }
+
+ public void append(int offset)
+ {
+ if (size == 0)
+ {
+ append(offset, offset);
+ return;
+ }
+
+ int tail = bounds[size - 1];
+ if (offset <= tail)
+ throw new IllegalArgumentException("Can't append " + offset + " to " + tail);
+
+ if (offset == tail + 1)
+ bounds[size-1] = offset;
+ else
+ append(offset, offset);
+ }
+ }
+
+ public static class Mutable extends AbstractMutable<Mutable>
+ {
+ private static final SetSupport.Adaptor<Mutable> adaptor = new SetSupport.Adaptor<Mutable>()
+ {
+ @Override
+ public Mutable empty(CoordinatorLogId logId)
+ {
+ return new Mutable(logId);
+ }
+
+ @Override
+ public Mutable passThrough(Offsets offsets)
+ {
+ if (offsets instanceof Mutable)
+ return (Mutable) offsets;
+ return copy(offsets);
+ }
+
+ @Override
+ public Mutable create(CoordinatorLogId logId, int[] bounds, int size)
+ {
+ return new Mutable(logId, bounds, size);
+ }
+ };
+
+ public Mutable(RangeIterator rangeIterator)
+ {
+ super(rangeIterator);
+ }
+
+ public Mutable(CoordinatorLogId logId)
+ {
+ super(logId);
+ }
+
+ public Mutable(CoordinatorLogId logId, int[] bounds)
+ {
+ super(logId, bounds);
+ }
+
+ public Mutable(CoordinatorLogId logId, int[] bounds, int size)
+ {
+ super(logId, bounds, size);
+ }
+
+ public static Mutable createOrNull(RangeIterator rangeIterator)
+ {
+ return AbstractMutable.createOrNull(rangeIterator, Mutable::new);
+ }
+
+ public static Mutable copy(Offsets offsets)
+ {
+ return new Mutable(offsets.logId, Arrays.copyOf(offsets.bounds, offsets.size));
+ }
+
+ public static Mutable union(Offsets a, Offsets b)
+ {
+ return Offsets.union(a, b, adaptor);
+ }
+
+ public static Mutable difference(Offsets a, Offsets b)
+ {
+ return Offsets.difference(a, b, adaptor);
+ }
+
+ public static Mutable intersection(Offsets a, Offsets b)
+ {
+ return Offsets.intersection(a, b, adaptor);
+ }
+ }
+
+ public static class Immutable extends Offsets
+ {
+ private static final SetSupport.Adaptor<Immutable> adaptor = new SetSupport.Adaptor<Immutable>()
+ {
+ @Override
+ public Immutable empty(CoordinatorLogId logId)
+ {
+ return new Immutable(logId);
+ }
+
+ @Override
+ public Immutable passThrough(Offsets offsets)
+ {
+ if (offsets instanceof Immutable)
+ return (Immutable) offsets;
+ return copy(offsets);
+ }
+
+ @Override
+ public Immutable create(CoordinatorLogId logId, int[] bounds, int size)
+ {
+ return new Immutable(logId, bounds, size);
+ }
+ };
+ private static int[] EMPTY = new int[0];
+
+ public Immutable(CoordinatorLogId logId, int[] bounds)
+ {
+ super(logId, bounds);
+ }
+
+ public Immutable(CoordinatorLogId logId, int[] bounds, int size)
+ {
+ super(logId, bounds, size);
+ }
+
+ public Immutable(CoordinatorLogId logId)
+ {
+ this(logId, EMPTY);
+ }
+
+ public static Immutable copy(Offsets offsets)
+ {
+ if (offsets instanceof Immutable)
+ return (Immutable) offsets;
+ return new Immutable(offsets.logId, Arrays.copyOf(offsets.bounds, offsets.size));
+ }
+
+ public static class Builder extends AbstractMutable<Builder>
+ {
+ public Builder(CoordinatorLogId logId)
+ {
+ super(logId);
+ }
+
+ public Builder(CoordinatorLogId logId, int[] bounds)
+ {
+ super(logId, bounds);
+ }
+
+ public Immutable build()
+ {
+ return new Immutable(logId, bounds, size);
+ }
+
+ public static Builder createOrNull(RangeIterator rangeIterator)
+ {
+ return AbstractMutable.createOrNull(rangeIterator, Builder::new);
+ }
+
+ public static Builder copy(Offsets offsets)
+ {
+ return new Builder(offsets.logId, Arrays.copyOf(offsets.bounds, offsets.size));
+ }
+ }
+
+ public static Immutable union(Offsets a, Offsets b)
+ {
+ return Offsets.union(a, b, adaptor);
+ }
+
+ public static Immutable difference(Offsets a, Offsets b)
+ {
+ return Offsets.difference(a, b, adaptor);
+ }
+
+ public static Immutable intersection(Offsets a, Offsets b)
+ {
+ return Offsets.intersection(a, b, adaptor);
+ }
+ }
+
+ private static int rangeStart(int range)
+ {
+ return range * 2;
+ }
+
+ private static int rangeEnd(int range)
+ {
+ return rangeStart(range) + 1;
+ }
+
+ public RangeIterator rangeIterator()
+ {
+ int numRanges = rangeCount();
+
+ return new RangeIterator()
+ {
+ int range = -1;
+
+ @Override
+ public int start()
+ {
+ Preconditions.checkState(range >= 0 && range <= numRanges);
+ return bounds[rangeStart(range)];
+ }
+
+ @Override
+ public int end()
+ {
+ Preconditions.checkState(range >= 0 && range <= numRanges);
+ return bounds[rangeEnd(range)];
+ }
+
+ @Override
+ public boolean tryAdvance()
+ {
+ if (isFinished())
+ return false;
+ range++;
+ return !isFinished();
+ }
+
+ @Override
+ public CoordinatorLogId logId()
+ {
+ return logId;
+ }
+
+ @Override
+ public boolean isFinished()
+ {
+ return range >= numRanges;
+ }
+ };
+ }
+
+ private static class SetSupport
+ {
+ private static final int NO_SPLIT_SENTINEL = Integer.MIN_VALUE;
+
+ interface Adaptor<O extends Offsets>
+ {
+ O empty(CoordinatorLogId logId);
+ O passThrough(Offsets offsets);
+ O create(CoordinatorLogId logId, int[] bounds, int size);
+ }
+
+ private enum RangeOverlap
+ {
+ BEFORE, BEFORE_ADJACENT, AFTER, AFTER_ADJACENT, INTERSECTING
+ }
+
+ private static RangeOverlap calculateRangeOverlap(int aStart, int aEnd, int bStart, int bEnd)
+ {
+ Preconditions.checkState(aStart <= aEnd);
+ Preconditions.checkState(bStart <= bEnd);
+
+ if (aEnd < bStart)
+ return aEnd == bStart - 1 ? RangeOverlap.BEFORE_ADJACENT : RangeOverlap.BEFORE;
+
+ if (bEnd < aStart)
+ return bEnd == aStart - 1 ? RangeOverlap.AFTER_ADJACENT : RangeOverlap.AFTER;
+
+ return RangeOverlap.INTERSECTING;
+ }
+
+ private static RangeOverlap calculateRangeOverlap(int aSplit, int aStart, int aEnd, int bSplit, int bStart, int bEnd)
+ {
+ aStart = aSplit != NO_SPLIT_SENTINEL ? aSplit : aStart;
+ bStart = bSplit != NO_SPLIT_SENTINEL ? bSplit : bStart;
+ return calculateRangeOverlap(aStart, aEnd, bStart, bEnd);
+ }
+
+ private static RangeOverlap calculateRangeOverlap(int aSplit, int[] a, int aRange, int bSplit, int[] b, int bRange)
+ {
+ return calculateRangeOverlap(aSplit, a[rangeStart(aRange)], a[rangeEnd(aRange)], bSplit, b[rangeStart(bRange)], b[rangeEnd(bRange)]);
+ }
+
+ private static RangeOverlap calculateRangeOverlap(int[] a, int aRange, int[] b, int bRange)
+ {
+ return calculateRangeOverlap(NO_SPLIT_SENTINEL, a, aRange, NO_SPLIT_SENTINEL, b, bRange);
+ }
+
+ private static int[] ensureCapacity(int[] offsets, int capacity, int expectedMaxCapacity)
+ {
+ Preconditions.checkArgument(capacity > 0);
+ if (capacity <= offsets.length)
+ return offsets;
+
+ int newCapacity = capacity * 2;
+
+ // if we overflowed, set to max
+ if (newCapacity < 0)
+ newCapacity = Integer.MAX_VALUE;
+
+ if (newCapacity > expectedMaxCapacity && offsets.length < expectedMaxCapacity)
+ newCapacity = expectedMaxCapacity;
+
+ int[] newOffsets = new int[newCapacity];
+ System.arraycopy(offsets, 0, newOffsets, 0, offsets.length);
+
+ return newOffsets;
+ }
+
+ private static <O extends Offsets> O addRemainder(CoordinatorLogId logId, int dstSplit, int[] dst, int dstRange, int[] src, int srcRange, int srcNumRanges, Adaptor<O> adaptor)
+ {
+ int capacity = (dstRange + srcNumRanges - srcRange) * 2;
+ dst = ensureCapacity(dst, capacity, capacity);
+ while (srcRange < srcNumRanges)
+ {
+ int addStart = dstSplit != NO_SPLIT_SENTINEL ? dstSplit : src[rangeStart(srcRange)];
+ int addEnd = src[rangeEnd(srcRange)];
+ // extend last range if possible
+ if (dstRange > 0 && addStart <= dst[rangeEnd(dstRange - 1)] + 1)
+ {
+ dst[rangeEnd(dstRange - 1)] = addEnd;
+ }
+ else
+ {
+ dst[rangeStart(dstRange)] = addStart;
+ dst[rangeEnd(dstRange)] = addEnd;
+ dstRange++;
+ }
+ dstSplit = NO_SPLIT_SENTINEL;
+ srcRange++;
+ }
+ return adaptor.create(logId, dst, dstRange * 2);
+ }
+
+ private static <O extends Offsets> O addRemainder(CoordinatorLogId logId, int[] dst, int dstRange, int[] src, int srcRange, int srcNumRanges, Adaptor<O> adaptor)
+ {
+ return addRemainder(logId, NO_SPLIT_SENTINEL, dst, dstRange, src, srcRange, srcNumRanges, adaptor);
+ }
+
+ private static int[] ensureCapacity(int[] offsets, int capacity)
+ {
+ return ensureCapacity(offsets, capacity, Integer.MAX_VALUE);
+ }
+ }
+
+ private static <O extends Offsets> O union(Offsets a, Offsets b, SetSupport.Adaptor<O> adaptor)
+ {
+ if (a == null)
+ return adaptor.passThrough(b);
+
+ if (b == null)
+ return adaptor.passThrough(a);
+
+ Preconditions.checkArgument(a.logId.equals(b.logId));
+ CoordinatorLogId logId = a.logId;
+
+ int aNumRanges = a.rangeCount();
+ int bNumRanges = b.rangeCount();
+
+ if (aNumRanges == 0)
+ return adaptor.passThrough(b);
+
+ if (bNumRanges == 0)
+ return adaptor.passThrough(a);
+
+ int aRange = 0;
+ int bRange = 0;
+
+ int cRange = 0;
+ int[] c = new int[Math.max(aNumRanges, bNumRanges) * 2];
+
+ while (aRange < aNumRanges && bRange < bNumRanges)
+ {
+ int addStart;
+ int addEnd;
+ SetSupport.RangeOverlap rangeOverlap = SetSupport.calculateRangeOverlap(a.bounds, aRange, b.bounds, bRange);
+ switch (rangeOverlap)
+ {
+ case BEFORE:
+ addStart = a.bounds[rangeStart(aRange)];
+ addEnd = a.bounds[rangeEnd(aRange)];
+ aRange++;
+ break;
+ case AFTER:
+ addStart = b.bounds[rangeStart(bRange)];
+ addEnd = b.bounds[rangeEnd(bRange)];
+ bRange++;
+ break;
+ case BEFORE_ADJACENT:
+ case AFTER_ADJACENT:
+ case INTERSECTING:
+ addStart = Math.min(a.bounds[rangeStart(aRange)], b.bounds[rangeStart(bRange)]);
+ addEnd = Math.max(a.bounds[rangeEnd(aRange)], b.bounds[rangeEnd(bRange)]);
+ aRange++;
+ bRange++;
+ break;
+ default:
+ throw new IllegalStateException("Unhandled union op: " + rangeOverlap);
+ }
+
+ // extend the tail if we can
+ if (cRange > 0 && addStart <= c[rangeEnd(cRange-1)] + 1)
+ {
+ c[rangeEnd(cRange - 1)] = addEnd;
+ }
+ else
+ {
+ c = SetSupport.ensureCapacity(c, (cRange + 1) * 2);
+ c[rangeStart(cRange)] = addStart;
+ c[rangeEnd(cRange)] = addEnd;
+ cRange++;
+ }
+ }
+
+ if (aRange < aNumRanges)
+ {
+ Preconditions.checkState(bRange == bNumRanges);
+ return SetSupport.addRemainder(logId, c, cRange, a.bounds, aRange, aNumRanges, adaptor);
+ }
+
+ if (bRange < bNumRanges)
+ {
+ Preconditions.checkState(aRange == aNumRanges);
+ return SetSupport.addRemainder(logId, c, cRange, b.bounds, bRange, bNumRanges, adaptor);
+ }
+
+ return adaptor.create(logId, c, cRange * 2);
+ }
+
+ private static abstract class AbstractSetIterator implements RangeIterator
+ {
+ enum State
+ {
+ INITIALIZED, VALID, FINISHED;
+
+ boolean isValid()
+ {
+ return this == VALID;
+ }
+
+ boolean isFinished()
+ {
+ return this == FINISHED;
+ }
+ }
+ final RangeIterator a;
+ final RangeIterator b;
+ int start;
+ int end;
+ private State state = State.INITIALIZED;
+
+ public AbstractSetIterator(RangeIterator a, RangeIterator b)
+ {
+ Preconditions.checkNotNull(a);
+ Preconditions.checkNotNull(b);
+ Preconditions.checkArgument(a.logId().equals(b.logId()));
+ this.a = a;
+ this.b = b;
+ }
+
+ @Override
+ public int start()
+ {
+ Preconditions.checkState(state.isValid());
+ return start;
+ }
+
+ @Override
+ public int end()
+ {
+ Preconditions.checkState(state.isValid());
+ return end;
+ }
+
+ protected abstract State computeNext();
+
+ @Override
+ public boolean tryAdvance()
+ {
+ switch (state)
+ {
+ case INITIALIZED:
+ a.tryAdvance();
+ b.tryAdvance();
+ state = State.VALID;
+ case VALID:
+ state = computeNext();
+ if (!state.isFinished())
+ break;;
+ case FINISHED:
+ return false;
+ default:
+ throw new IllegalStateException("Unhandled state: " + state);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean isFinished()
+ {
+ return state.isFinished();
+ }
+
+ @Override
+ public CoordinatorLogId logId()
+ {
+ return a.logId();
+ }
+ }
+
+ private static class UnionIterator extends AbstractSetIterator
+ {
+ public UnionIterator(RangeIterator a, RangeIterator b)
+ {
+ super(a, b);
+ }
+
+ void extendEnd(RangeIterator iter)
+ {
+ while (iter.tryAdvance())
+ {
+ SetSupport.RangeOverlap rangeOverlap = SetSupport.calculateRangeOverlap(iter.start(), iter.end(), start, end);
+ switch (rangeOverlap)
+ {
+ case BEFORE:
+ case BEFORE_ADJACENT:
+ throw new IllegalStateException();
+ case AFTER:
+ return;
+ case INTERSECTING:
+ case AFTER_ADJACENT:
+ end = Math.max(end, iter.end());
+ continue;
+ default:
+ throw new IllegalStateException("Unhandled union op: " + rangeOverlap);
+ }
+ }
+ }
+
+ @Override
+ protected State computeNext()
+ {
+
+ if (a.isFinished())
+ {
+ if (b.isFinished())
+ return State.FINISHED;
+
+ start = b.start();
+ end = b.end();
+ b.tryAdvance();
+ return State.VALID;
+ }
+
+ if (b.isFinished())
+ {
+ start = a.start();
+ end = a.end();
+ a.tryAdvance();
+ return State.VALID;
+ }
+
+ SetSupport.RangeOverlap rangeOverlap = SetSupport.calculateRangeOverlap(a.start(), a.end(), b.start(), b.end());
+ switch (rangeOverlap)
+ {
+ case BEFORE:
+ start = a.start();
+ end = a.end();
+ a.tryAdvance();
+ break;
+ case AFTER:
+ start = b.start();
+ end = b.end();
+ b.tryAdvance();
+ break;
+ case BEFORE_ADJACENT:
+ case AFTER_ADJACENT:
+ case INTERSECTING:
+ start = Math.min(a.start(), b.start());
+ end = Math.max(a.end(), b.end());
+ extendEnd(a);
+ extendEnd(b);
+ break;
+ default:
+ throw new IllegalStateException("Unhandled union op: " + rangeOverlap);
+ }
+
+ return State.VALID;
+ }
+ }
+
+ public static RangeIterator union(RangeIterator a, RangeIterator b)
+ {
+ return new UnionIterator(a, b);
+ }
+
+ /**
+ * Subtract b from a
+ */
+ private static <O extends Offsets> O difference(Offsets a, Offsets b, SetSupport.Adaptor<O> adaptor)
+ {
+ Preconditions.checkArgument(a != null);
+ if (b == null)
+ return adaptor.passThrough(a);
+
+ Preconditions.checkArgument(b == null || a.logId.equals(b.logId));
+ CoordinatorLogId logId = a.logId;
+ int aNumRanges = a.rangeCount();
+ int bNumRanges = b.rangeCount();
+
+ if (aNumRanges == 0)
+ return adaptor.empty(logId);
+
+ if (bNumRanges == 0)
+ return adaptor.passThrough(a);
+
+ int aRange = 0;
+ int bRange = 0;
+
+ int cRange = 0;
+ int[] c = new int[Math.max(aNumRanges, bNumRanges) * 2];
+
+ int aSplit = SetSupport.NO_SPLIT_SENTINEL;
+ while (aRange < aNumRanges && bRange < bNumRanges)
+ {
+ int addStart;
+ int addEnd;
+
+ SetSupport.RangeOverlap rangeOverlap = SetSupport.calculateRangeOverlap(aSplit, a.bounds, aRange, SetSupport.NO_SPLIT_SENTINEL, b.bounds, bRange);
+ switch (rangeOverlap)
+ {
+ case BEFORE:
+ case BEFORE_ADJACENT:
+ addStart = aSplit != SetSupport.NO_SPLIT_SENTINEL ? aSplit : a.bounds[rangeStart(aRange)];
+ addEnd = a.bounds[rangeEnd(aRange)];
+ aSplit = SetSupport.NO_SPLIT_SENTINEL;
+ aRange++;
+ break;
+ case AFTER:
+ case AFTER_ADJACENT:
+ aSplit = SetSupport.NO_SPLIT_SENTINEL;
+ bRange++;
+ continue;
+ case INTERSECTING:
+ int aStart = aSplit != SetSupport.NO_SPLIT_SENTINEL ? aSplit : a.bounds[rangeStart(aRange)];
+ int aEnd = a.bounds[rangeEnd(aRange)];
+ int bStart = b.bounds[rangeStart(bRange)];
+ int bEnd = b.bounds[rangeEnd(bRange)];
+
+ if (bStart <= aStart)
+ {
+ if (bEnd >= aEnd)
+ {
+ // b consumes the entire a range
+ aSplit = SetSupport.NO_SPLIT_SENTINEL;
+ aRange++;
+ }
+ else
+ {
+ // set the split and start over, the next b range may intersect with the current range
+ aSplit = bEnd + 1;
+ bRange++;
+ }
+ continue;
+ }
+
+ // does not consume the start of the range
+ Preconditions.checkState(bStart > aStart);
+ addStart = aStart;
+ addEnd = bStart - 1;
+
+ if (bEnd < aEnd)
+ {
+ // b splits the range
+ aSplit = bEnd + 1;
+ bRange++;
+ }
+ else
+ {
+ // b consumes the rest of the a range
+ aSplit = SetSupport.NO_SPLIT_SENTINEL;
+ aRange++;
+ }
+ break;
+ default:
+ throw new IllegalStateException("Unhandled union op: " + rangeOverlap);
+ }
+
+ // extend the tail if we can
+ if (cRange > 0 && addStart <= c[rangeEnd(cRange-1)] + 1)
+ {
+ c[rangeEnd(cRange - 1)] = addEnd;
+ }
+ else
+ {
+ c = SetSupport.ensureCapacity(c, (cRange + 1) * 2);
+ c[rangeStart(cRange)] = addStart;
+ c[rangeEnd(cRange)] = addEnd;
+ cRange++;
+ }
+ }
+
+ if (aRange < aNumRanges)
+ {
+ Preconditions.checkState(bRange == bNumRanges);
+ return SetSupport.addRemainder(logId, aSplit, c, cRange, a.bounds, aRange, aNumRanges, adaptor);
+ }
+
+ return adaptor.create(logId, c, cRange * 2);
+ }
+
+ private static class DifferenceIterator extends AbstractSetIterator
+ {
+ private int aSplit = SetSupport.NO_SPLIT_SENTINEL;
+
+ public DifferenceIterator(RangeIterator a, RangeIterator b)
+ {
+ super(a, b);
+ }
+
+ @Override
+ protected State computeNext()
+ {
+ while (!a.isFinished() && !b.isFinished())
+ {
+ SetSupport.RangeOverlap rangeOverlap = SetSupport.calculateRangeOverlap(aSplit, a.start(), a.end(),
+ SetSupport.NO_SPLIT_SENTINEL, b.start(), b.end());
+ switch (rangeOverlap)
+ {
+ case BEFORE:
+ case BEFORE_ADJACENT:
+ start = aSplit != SetSupport.NO_SPLIT_SENTINEL ? aSplit : a.start();
+ end = a.end();
+ aSplit = SetSupport.NO_SPLIT_SENTINEL;
+ a.tryAdvance();
+ return State.VALID;
+ case AFTER:
+ case AFTER_ADJACENT:
+ aSplit = SetSupport.NO_SPLIT_SENTINEL;
+ b.tryAdvance();
+ continue;
+ case INTERSECTING:
+ int aStart = aSplit != SetSupport.NO_SPLIT_SENTINEL ? aSplit : a.start();
+ int aEnd = a.end();
+ int bStart = b.start();
+ int bEnd = b.end();
+
+ if (bStart <= aStart)
+ {
+ if (bEnd >= aEnd)
+ {
+ // b consumes the entire a range
+ aSplit = SetSupport.NO_SPLIT_SENTINEL;
+ a.tryAdvance();
+ }
+ else
+ {
+ // set the split and start over, the next b range may intersect with the current range
+ aSplit = bEnd + 1;
+ b.tryAdvance();
+ }
+ continue;
+ }
+
+ // does not consume the start of the range
+ Preconditions.checkState(bStart > aStart);
+ start = aStart;
+ end = bStart - 1;
+
+ if (bEnd < aEnd)
+ {
+ // b splits the range
+ aSplit = bEnd + 1;
+ b.tryAdvance();
+ }
+ else
+ {
+ // b consumes the rest of the a range
+ aSplit = SetSupport.NO_SPLIT_SENTINEL;
+ a.tryAdvance();
+ }
+ return State.VALID;
+ default:
+ throw new IllegalStateException("Unhandled union op: " + rangeOverlap);
+ }
+ }
+
+ if (a.isFinished())
+ return State.FINISHED;
+
+ if (b.isFinished())
+ {
+ start = aSplit != SetSupport.NO_SPLIT_SENTINEL ? aSplit : a.start();
+ end = a.end();
+ aSplit = SetSupport.NO_SPLIT_SENTINEL;
+ a.tryAdvance();
+ return State.VALID;
+ }
+
+ // shouldn't be possible to get here
+ throw new IllegalStateException();
+ }
+ }
+
+ public static RangeIterator difference(RangeIterator a, RangeIterator b)
+ {
+ return new DifferenceIterator(a, b);
+ }
+
+ private static <O extends Offsets> O intersection(Offsets a, Offsets b, SetSupport.Adaptor<O> adaptor)
+ {
+ Preconditions.checkArgument(a.logId.equals(b.logId));
+ CoordinatorLogId logId = a.logId;
+
+ int aNumRanges = a.rangeCount();
+ int bNumRanges = b.rangeCount();
+
+ if (aNumRanges == 0 || bNumRanges == 0)
+ return adaptor.empty(logId);
+
+ int aRange = 0;
+ int bRange = 0;
+
+ int cRange = 0;
+ int[] c = new int[Math.max(aNumRanges, bNumRanges) * 2];
+
+ int aSplit = SetSupport.NO_SPLIT_SENTINEL;
+ int bSplit = SetSupport.NO_SPLIT_SENTINEL;
+ while (aRange < aNumRanges && bRange < bNumRanges)
+ {
+ int addStart;
+ int addEnd;
+
+ SetSupport.RangeOverlap rangeOverlap = SetSupport.calculateRangeOverlap(aSplit, a.bounds, aRange, bSplit, b.bounds, bRange);
+ switch (rangeOverlap)
+ {
+ case BEFORE:
+ case BEFORE_ADJACENT:
+ aSplit = SetSupport.NO_SPLIT_SENTINEL;
+ bSplit = SetSupport.NO_SPLIT_SENTINEL;
+ aRange++;
+ continue;
+ case AFTER:
+ case AFTER_ADJACENT:
+ aSplit = SetSupport.NO_SPLIT_SENTINEL;
+ bSplit = SetSupport.NO_SPLIT_SENTINEL;
+ bRange++;
+ continue;
+ case INTERSECTING:
+ int aStart = aSplit != SetSupport.NO_SPLIT_SENTINEL ? aSplit : a.bounds[rangeStart(aRange)];
+ int aEnd = a.bounds[rangeEnd(aRange)];
+ int bStart = bSplit != SetSupport.NO_SPLIT_SENTINEL ? bSplit : b.bounds[rangeStart(bRange)];
+ int bEnd = b.bounds[rangeEnd(bRange)];
+
+ addStart = Math.max(aStart, bStart);
+ addEnd = Math.min(aEnd, bEnd);
+
+ if (aEnd < bEnd)
+ {
+ aSplit = SetSupport.NO_SPLIT_SENTINEL;
+ bSplit = aEnd;
+ aRange++;
+ }
+ else if (bEnd < aEnd)
+ {
+ aSplit = bEnd;
+ bSplit = SetSupport.NO_SPLIT_SENTINEL;
+ bRange++;
+ }
+ else
+ {
+ aSplit = SetSupport.NO_SPLIT_SENTINEL;
+ bSplit = SetSupport.NO_SPLIT_SENTINEL;
+ aRange++;
+ bRange++;
+ }
+ break;
+
+ default:
+ throw new IllegalStateException("Unhandled union op: " + rangeOverlap);
+ }
+
+ // extend the tail if we can, though it shouldn't be possible unless one of the input lists were malformed
+ if (cRange > 0 && addStart <= c[rangeEnd(cRange-1)] + 1)
+ {
+ c[rangeEnd(cRange - 1)] = addEnd;
+ }
+ else
+ {
+ c = SetSupport.ensureCapacity(c, (cRange + 1) * 2);
+ c[rangeStart(cRange)] = addStart;
+ c[rangeEnd(cRange)] = addEnd;
+ cRange++;
+ }
+ }
+
+ return adaptor.create(logId, c, cRange * 2);
+ }
+
+ public interface OffsetReciever
+ {
+ boolean add(int offset);
+
+ void addAll(Offsets offsets);
+ }
+
+ public interface OffsetConsumer
+ {
+ void accept(CoordinatorLogId logId, int offset);
+ }
+
+ public interface RangeConsumer
+ {
+ RangeConsumer NONE = (log, start, end) -> {};
+
+ void consume(CoordinatorLogId logId, int startOffset, int endOffset);
+ }
+
+ /**
+ * Intrusive iterator. Adjacent ranges may or may not be merged
+ *
+ * iterator is created in an invalid state, and tryAdvance needs to be called before calling start or
+ * stop. This is to support empty iterators, and makes iterating through the contents less verbose than
+ * have to check isFinished before reading the values, and makes iterator initialization less error-prone;
+ */
+ public interface RangeIterator
+ {
+ int start();
+ int end();
+ boolean tryAdvance();
+ boolean isFinished();
+ CoordinatorLogId logId();
+ }
+
+ // TODO (consider): delta-encoding + vints
+ public static final IVersionedSerializer<Offsets.Immutable> serializer = new IVersionedSerializer<>()
+ {
+ @Override
+ public void serialize(Offsets.Immutable offsets, DataOutputPlus out, int version) throws IOException
+ {
+ CoordinatorLogId.serializer.serialize(offsets.logId, out, version);
+ out.writeInt(offsets.size);
+ for (int i = 0; i < offsets.size; i++)
+ out.writeInt(offsets.bounds[i]);
+ }
+
+ @Override
+ public Offsets.Immutable deserialize(DataInputPlus in, int version) throws IOException
+ {
+ CoordinatorLogId logId = CoordinatorLogId.serializer.deserialize(in, version);
+ int size = in.readInt();
+ Preconditions.checkArgument(size >= 0 && size % 2 == 0);
+ int[] bounds = new int[size];
+ for (int i = 0; i < size; i++)
+ bounds[i] = in.readInt();
+ return new Offsets.Immutable(logId, bounds);
+ }
+
+ @Override
+ public long serializedSize(Offsets.Immutable offsets, int version)
+ {
+ long size = CoordinatorLogId.serializer.serializedSize(offsets.logId, version);
+ size += TypeSizes.sizeof(offsets.size);
+ size += (long) TypeSizes.INT_SIZE * offsets.size;
+ return size;
+ }
+ };
+}
diff --git a/src/java/org/apache/cassandra/replication/Participants.java b/src/java/org/apache/cassandra/replication/Participants.java
new file mode 100644
index 0000000000..c665d63a99
--- /dev/null
+++ b/src/java/org/apache/cassandra/replication/Participants.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.replication;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+public class Participants
+{
+ private final int[] hosts;
+
+ Participants(Collection<Integer> participants)
+ {
+ int i = 0;
+ int[] hosts = new int[participants.size()];
+ for (int host : participants) hosts[i++] = host;
+ Arrays.sort(hosts);
+ this.hosts = hosts;
+ }
+
+ int size()
+ {
+ return hosts.length;
+ }
+
+ int indexOf(int hostId)
+ {
+ int idx = Arrays.binarySearch(hosts, hostId);
+ if (idx < 0)
+ throw new IllegalArgumentException("Unknown host id " + hostId);
+ return idx;
+ }
+
+ int get(int idx)
+ {
+ if (idx < 0 || idx >= hosts.length)
+ throw new IllegalArgumentException("Out of bounds host idx " + idx);
+ return hosts[idx];
+ }
+}
diff --git a/src/java/org/apache/cassandra/replication/ReconciliationPlan.java b/src/java/org/apache/cassandra/replication/ReconciliationPlan.java
new file mode 100644
index 0000000000..ac085bf839
--- /dev/null
+++ b/src/java/org/apache/cassandra/replication/ReconciliationPlan.java
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.replication;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.locator.InetAddressAndPort;
+import org.apache.cassandra.replication.MutationSummary.CoordinatorSummary;
+
+public class ReconciliationPlan
+{
+ private final ImmutableMap<InetAddressAndPort, Log2OffsetsMap.Immutable> txPlan;
+
+ public ReconciliationPlan(ImmutableMap<InetAddressAndPort, Log2OffsetsMap.Immutable> txPlan)
+ {
+ this.txPlan = txPlan;
+ }
+
+ public Set<InetAddressAndPort> nodes()
+ {
+ return txPlan.keySet();
+ }
+
+ public Log2OffsetsMap.Immutable peerReconciliation(InetAddressAndPort to)
+ {
+ return txPlan.get(to);
+ }
+
+ public Log2OffsetsMap.Immutable offsetsFor(InetAddressAndPort node)
+ {
+ return txPlan.get(node);
+ }
+
+ public boolean isEmpty()
+ {
+ return txPlan.isEmpty();
+ }
+
+ private static class PlanBuilder
+ {
+ final InetAddressAndPort node;
+
+ final MutationSummary summary;
+ final Map<InetAddressAndPort, Log2OffsetsMap.Immutable.Builder> peerReconciliations = new HashMap<>();
+
+ public PlanBuilder(InetAddressAndPort node, MutationSummary summary)
+ {
+ this.node = node;
+ this.summary = summary;
+ }
+
+ public void send(InetAddressAndPort to, Offsets sequenceIds)
+ {
+ peerReconciliations.computeIfAbsent(to, ep -> new Log2OffsetsMap.Immutable.Builder()).add(sequenceIds);
+ }
+
+ ReconciliationPlan build()
+ {
+ ImmutableMap.Builder<InetAddressAndPort, Log2OffsetsMap.Immutable> builder = ImmutableMap.builder();
+ peerReconciliations.forEach((to, ids) -> builder.put(to, ids.build()));
+ return new ReconciliationPlan(builder.build());
+ }
+ }
+
+ // TODO (desired): rework Offsets set logic usage to use Offsets#RangeIterator instead of rematerializing sets
+ private static class CoordinatorLogReconciliation
+ {
+ final CoordinatorLogId logId;
+ Offsets.Immutable reconciled;
+ Offsets.Immutable unreconciled;
+
+ Map<InetAddressAndPort, Offsets> unreconciledNodes = new HashMap<>();
+
+ CoordinatorLogReconciliation(CoordinatorLogId logId)
+ {
+ this.logId = logId;
+ }
+
+ void addPeerSummary(InetAddressAndPort peer, CoordinatorSummary summary)
+ {
+ Preconditions.checkArgument(summary.logId().equals(logId));
+ reconciled = Offsets.Immutable.union(reconciled, summary.reconciled);
+ unreconciled = Offsets.Immutable.union(unreconciled, summary.unreconciled);
+ unreconciledNodes.put(peer, summary.unreconciled);
+ }
+
+ void createPlan(Map<InetAddressAndPort, PlanBuilder> plan)
+ {
+ // remove reconciled ids
+ Offsets.Immutable allIds = Offsets.Immutable.difference(unreconciled, reconciled);
+ for (InetAddressAndPort receiver : plan.keySet())
+ {
+ Offsets.Immutable missing = Offsets.Immutable.difference(allIds, unreconciledNodes.get(receiver));
+ if (missing.isEmpty())
+ continue;
+
+ // TODO: look into more intelligent ways to distribute mutation requests
+ for (Map.Entry<InetAddressAndPort, Offsets> sender : unreconciledNodes.entrySet())
+ {
+ if (sender.getKey().equals(receiver))
+ continue;
+
+ Offsets senderIds = sender.getValue();
+ PlanBuilder senderPlan = plan.get(sender.getKey());
+
+ Offsets.Immutable requestedIds = Offsets.Immutable.intersection(missing, senderIds);
+ senderPlan.send(receiver, requestedIds);
+
+ missing = Offsets.Immutable.difference(missing, requestedIds);
+ if (missing.rangeCount() == 0)
+ break;
+ }
+ }
+ }
+ }
+
+ public static Map<InetAddressAndPort, ReconciliationPlan> calculateReconciliation(Map<InetAddressAndPort, MutationSummary> summaries)
+ {
+ Map<InetAddressAndPort, PlanBuilder> planBuilders = new HashMap<>();
+ Map<CoordinatorLogId, CoordinatorLogReconciliation> coordinatorReconciliations = new HashMap<>();
+
+ // organize data by peer and log id
+ summaries.forEach((node, summary) -> {
+
+ planBuilders.put(node, new PlanBuilder(node, summary));
+
+ for (int i=0; i<summary.size(); i++)
+ {
+ CoordinatorSummary coordinatorSummary = summary.get(i);
+ CoordinatorLogReconciliation reconciliation = coordinatorReconciliations.computeIfAbsent(coordinatorSummary.logId(), CoordinatorLogReconciliation::new);
+ reconciliation.addPeerSummary(node, coordinatorSummary);
+ }
+ });
+
+ coordinatorReconciliations.values().forEach(planBuilder -> planBuilder.createPlan(planBuilders));
+
+ Map<InetAddressAndPort, ReconciliationPlan> plans = new HashMap<>();
+ planBuilders.forEach((node, planBuilder) -> {
+ ReconciliationPlan plan = planBuilder.build();
+ if (!plan.isEmpty())
+ plans.put(node, plan);
+ });
+ return plans;
+ }
+
+ public static final IVersionedSerializer<ReconciliationPlan> serializer = new IVersionedSerializer<>()
+ {
+ @Override
+ public void serialize(ReconciliationPlan plan, DataOutputPlus out, int version) throws IOException
+ {
+ out.writeInt(plan.txPlan.size());
+ for (Map.Entry<InetAddressAndPort, Log2OffsetsMap.Immutable> entry : plan.txPlan.entrySet())
+ {
+ InetAddressAndPort.Serializer.inetAddressAndPortSerializer.serialize(entry.getKey(), out, version);
+ Log2OffsetsMap.Immutable.serializer.serialize(entry.getValue(), out, version);
+ }
+ }
+
+ @Override
+ public ReconciliationPlan deserialize(DataInputPlus in, int version) throws IOException
+ {
+ int size = in.readInt();
+ ImmutableMap.Builder<InetAddressAndPort, Log2OffsetsMap.Immutable> builder = ImmutableMap.builderWithExpectedSize(size);
+ for (int i = 0; i < size; i++)
+ {
+ InetAddressAndPort endpoint = InetAddressAndPort.Serializer.inetAddressAndPortSerializer.deserialize(in, version);
+ Log2OffsetsMap.Immutable offsets = Log2OffsetsMap.Immutable.serializer.deserialize(in, version);
+ builder.put(endpoint, offsets);
+ }
+ return new ReconciliationPlan(builder.build());
+ }
+
+ @Override
+ public long serializedSize(ReconciliationPlan plan, int version)
+ {
+ long size = TypeSizes.sizeof(plan.txPlan.size());
+ for (Map.Entry<InetAddressAndPort, Log2OffsetsMap.Immutable> entry : plan.txPlan.entrySet())
+ {
+ size += InetAddressAndPort.Serializer.inetAddressAndPortSerializer.serializedSize(entry.getKey(), version);
+ size += Log2OffsetsMap.Immutable.serializer.serializedSize(entry.getValue(), version);
+ }
+ return size;
+ }
+ };
+}
diff --git a/src/java/org/apache/cassandra/replication/Shard.java b/src/java/org/apache/cassandra/replication/Shard.java
new file mode 100644
index 0000000000..538e9a57c0
--- /dev/null
+++ b/src/java/org/apache/cassandra/replication/Shard.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.replication;
+
+import java.util.function.IntSupplier;
+
+import com.google.common.base.Preconditions;
+
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.PartitionPosition;
+import org.apache.cassandra.dht.AbstractBounds;
+import org.apache.cassandra.dht.Range;
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.locator.InetAddressAndPort;
+import org.apache.cassandra.replication.CoordinatorLog.CoordinatorLogPrimary;
+import org.apache.cassandra.tcm.ClusterMetadata;
+import org.apache.cassandra.tcm.Epoch;
+import org.jctools.maps.NonBlockingHashMapLong;
+
+public class Shard
+{
+ private final String keyspace;
+ private final Range<Token> tokenRange;
+ private final int localHostId;
+ private final Participants participants;
+ private final Epoch sinceEpoch;
+ private final NonBlockingHashMapLong<CoordinatorLog> logs;
+ // TODO (expected): add support for log rotation
+ private final CoordinatorLogPrimary currentLocalLog;
+
+ Shard(String keyspace, Range<Token> tokenRange, int localHostId, Participants participants, Epoch sinceEpoch, IntSupplier logIdProvider)
+ {
+ this.keyspace = keyspace;
+ this.tokenRange = tokenRange;
+ this.localHostId = localHostId;
+ this.participants = participants;
+ this.sinceEpoch = sinceEpoch;
+ this.logs = new NonBlockingHashMapLong<>();
+ this.currentLocalLog = startNewLog(localHostId, logIdProvider.getAsInt(), participants);
+ logs.put(currentLocalLog.logId.asLong(), currentLocalLog);
+ }
+
+ MutationId nextId()
+ {
+ return currentLocalLog.nextId();
+ }
+
+ void witnessedRemoteMutation(MutationId mutationId, InetAddressAndPort onHost)
+ {
+ int onHostId = ClusterMetadata.current().directory.peerId(onHost).id();
+ get(mutationId).witnessedRemoteMutation(mutationId, onHostId);
+ }
+
+ void startWriting(Mutation mutation)
+ {
+ get(mutation.id()).startWriting(mutation);
+ }
+
+ void finishWriting(Mutation mutation)
+ {
+ get(mutation.id()).finishWriting(mutation);
+ }
+
+ void addSummaryForKey(Token token, boolean includePending, MutationSummary.Builder builder)
+ {
+ logs.forEach((id, log) -> {
+ MutationSummary.CoordinatorSummary.Builder summaryBuilder = builder.builderForLog(log.logId);
+ log.collectOffsetsFor(token, builder.tableId, includePending, summaryBuilder.unreconciled, summaryBuilder.reconciled);
+ });
+ }
+
+ void addSummaryForRange(AbstractBounds<PartitionPosition> range, boolean includePending, MutationSummary.Builder builder)
+ {
+ logs.forEach((id, log) -> {
+ MutationSummary.CoordinatorSummary.Builder summaryBuilder = builder.builderForLog(log.logId);
+ log.collectOffsetsFor(range, builder.tableId, includePending, summaryBuilder.unreconciled, summaryBuilder.reconciled);
+ });
+ }
+
+ /**
+ * Creates a new coordinator log for this host. Primarily on Shard init (node startup or topology change).
+ * Also on keyspace creation.
+ */
+ private static CoordinatorLog.CoordinatorLogPrimary startNewLog(int localHostId, int hostLogId, Participants participants)
+ {
+ CoordinatorLogId logId = new CoordinatorLogId(localHostId, hostLogId);
+ return new CoordinatorLog.CoordinatorLogPrimary(localHostId, logId, participants);
+ }
+
+ private CoordinatorLog get(MutationId mutationId)
+ {
+ Preconditions.checkArgument(!mutationId.isNone());
+ return get(mutationId.logId());
+ }
+
+ private CoordinatorLog get(long logId)
+ {
+ CoordinatorLog log = logs.get(logId);
+ return log != null
+ ? log : logs.computeIfAbsent(logId, ignore -> CoordinatorLog.create(localHostId, new CoordinatorLogId(logId), participants));
+ }
+}
diff --git a/src/java/org/apache/cassandra/replication/ShortMutationId.java b/src/java/org/apache/cassandra/replication/ShortMutationId.java
new file mode 100644
index 0000000000..93ea294cbe
--- /dev/null
+++ b/src/java/org/apache/cassandra/replication/ShortMutationId.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.replication;
+
+import java.io.IOException;
+import java.util.Comparator;
+
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+
+/**
+ * MutationId without the timestamp component. This is sufficient for uniquely identifying a mutation,
+ * and for lookup in the journal and most tracking data structures.
+ */
+public class ShortMutationId extends CoordinatorLogId
+{
+ /**
+ * 4 byte offset. Offest is incremented, is alone is sufficient to identify
+ * the entry within a coordinator log.
+ * MutationId adds a timestamp for correlation purposes.
+ */
+ protected final int offset;
+
+ public ShortMutationId(long logId, int offset)
+ {
+ super(logId);
+ this.offset = offset;
+ }
+
+ public ShortMutationId(CoordinatorLogId logId, int offset)
+ {
+ super(logId.hostId(), logId.hostLogId());
+ this.offset = offset;
+ }
+
+ public ShortMutationId(MutationId mutationId)
+ {
+ this(mutationId.logId(), mutationId.offset());
+ }
+
+ public long logId()
+ {
+ return super.asLong();
+ }
+
+ public int offset()
+ {
+ return offset;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (!(o instanceof ShortMutationId)) return false;
+ ShortMutationId that = (ShortMutationId) o;
+ return this.logId() == that.logId() && this.offset == that.offset;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Integer.hashCode(offset) + 31 * super.hashCode();
+ }
+
+ @Override
+ public String toString()
+ {
+ return "ShortMutationId{" + hostId() + ", " + hostLogId() + ", " + offset() + '}';
+ }
+
+ public static final Comparator<ShortMutationId> comparator = (l, r) -> {
+ int cmp = CoordinatorLogId.comparator.compare(l, r);
+ return cmp != 0 ? cmp : Integer.compare(l.offset, r.offset);
+ };
+
+ public static final IVersionedSerializer<ShortMutationId> serializer = new IVersionedSerializer<>()
+ {
+ @Override
+ public void serialize(ShortMutationId id, DataOutputPlus out, int version) throws IOException
+ {
+ out.writeLong(id.logId());
+ out.writeInt(id.offset());
+ }
+
+ @Override
+ public ShortMutationId deserialize(DataInputPlus in, int version) throws IOException
+ {
+ long logId = in.readLong();
+ int offset = in.readInt();
+ return new ShortMutationId(logId, offset);
+ }
+
+ @Override
+ public long serializedSize(ShortMutationId id, int version)
+ {
+ return TypeSizes.sizeof(id.logId()) + TypeSizes.sizeof(id.offset());
+ }
+ };
+}
diff --git a/src/java/org/apache/cassandra/replication/TrackedWriteRequest.java b/src/java/org/apache/cassandra/replication/TrackedWriteRequest.java
new file mode 100644
index 0000000000..5ff3acb34e
--- /dev/null
+++ b/src/java/org/apache/cassandra/replication/TrackedWriteRequest.java
@@ -0,0 +1,287 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.replication;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ThreadLocalRandom;
+
+import com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.concurrent.DebuggableTask;
+import org.apache.cassandra.concurrent.Stage;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.ConsistencyLevel;
+import org.apache.cassandra.db.IMutation;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.WriteType;
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.exceptions.RequestFailureReason;
+import org.apache.cassandra.exceptions.WriteTimeoutException;
+import org.apache.cassandra.locator.AbstractReplicationStrategy;
+import org.apache.cassandra.locator.DynamicEndpointSnitch;
+import org.apache.cassandra.locator.EndpointsForToken;
+import org.apache.cassandra.locator.Replica;
+import org.apache.cassandra.locator.ReplicaPlan;
+import org.apache.cassandra.locator.ReplicaPlans;
+import org.apache.cassandra.net.ForwardingInfo;
+import org.apache.cassandra.net.Message;
+import org.apache.cassandra.net.MessageFlag;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.net.Verb;
+import org.apache.cassandra.service.TrackedWriteResponseHandler;
+import org.apache.cassandra.tracing.Tracing;
+import org.apache.cassandra.transport.Dispatcher;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.MonotonicClock;
+
+import static java.util.Collections.singletonList;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.writeMetrics;
+import static org.apache.cassandra.net.Verb.MUTATION_REQ;
+
+public class TrackedWriteRequest
+{
+ private static final Logger logger = LoggerFactory.getLogger(TrackedWriteRequest.class);
+
+ /**
+ * Coordinate write of a tracked mutation. Assumes the replica is a coordinator.
+ *
+ * @param mutation the mutation to be applied
+ * @param consistencyLevel the consistency level for the write operation
+ * @param requestTime object holding times when request got enqueued and started execution
+ */
+ public static TrackedWriteResponseHandler perform(
+ Mutation mutation, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime)
+ {
+ Tracing.trace("Determining replicas for mutation");
+
+ String keyspaceName = mutation.getKeyspaceName();
+ Keyspace keyspace = Keyspace.open(keyspaceName);
+ Token token = mutation.key().getToken();
+
+ MutationId id = MutationTrackingService.instance.nextMutationId(keyspaceName, token);
+ mutation = mutation.withMutationId(id);
+
+ ReplicaPlan.ForWrite plan = ReplicaPlans.forWrite(keyspace, consistencyLevel, token, ReplicaPlans.writeNormal);
+
+ if (plan.lookup(FBUtilities.getBroadcastAddressAndPort()) != null)
+ writeMetrics.localRequests.mark();
+ else
+ writeMetrics.remoteRequests.mark();
+
+ AbstractReplicationStrategy rs = plan.replicationStrategy();
+
+ TrackedWriteResponseHandler handler =
+ TrackedWriteResponseHandler.wrap(rs.getWriteResponseHandler(plan, null, WriteType.SIMPLE, null, requestTime),
+ keyspaceName,
+ mutation.key().getToken(),
+ id);
+ applyLocallyAndSendToReplicas(mutation, plan, handler);
+
+ return handler;
+ }
+
+ public static void applyLocallyAndSendToReplicas(Mutation mutation, ReplicaPlan.ForWrite plan, TrackedWriteResponseHandler handler)
+ {
+ String localDataCenter = DatabaseDescriptor.getLocator().local().datacenter;
+
+ boolean applyLocally = false;
+
+ // this DC replicas
+ List<Replica> localDCReplicas = null;
+
+ // extra-DC, grouped by DC
+ Map<String, List<Replica>> remoteDCReplicas = null;
+
+ // only need to create a Message for non-local writes
+ Message<Mutation> message = null;
+
+ // For performance, Mutation caches serialized buffers that are computed lazily in serializedBuffer(). That
+ // computation is not synchronized however, and we will potentially call that method concurrently for each
+ // dispatched message (not that concurrent calls to serializedBuffer() are "unsafe" per se, just that they
+ // may result in multiple computations, making the caching optimization moot). So forcing the serialization
+ // here to make sure it's already cached/computed when it's concurrently used later.
+ // Side note: we have one cached buffers for each used EncodingVersion and this only pre-compute the one for
+ // the current version, but it's just an optimization, and we're ok not optimizing for mixed-version clusters.
+ Mutation.serializer.prepareSerializedBuffer(mutation, MessagingService.current_version);
+
+ for (Replica destination : plan.contacts())
+ {
+ if (!plan.isAlive(destination))
+ {
+ handler.expired(); // immediately mark the response as expired since the request will not be sent
+ continue;
+ }
+
+ if (destination.isSelf())
+ {
+ applyLocally = true;
+ continue;
+ }
+
+ if (message == null)
+ message = Message.outWithFlags(MUTATION_REQ, mutation, handler.getRequestTime(), singletonList(MessageFlag.CALL_BACK_ON_FAILURE));
+
+ String dc = DatabaseDescriptor.getLocator().location(destination.endpoint()).datacenter;
+
+ if (localDataCenter.equals(dc))
+ {
+ if (localDCReplicas == null)
+ localDCReplicas = new ArrayList<>(plan.contacts().size());
+ localDCReplicas.add(destination);
+ }
+ else
+ {
+ if (remoteDCReplicas == null)
+ remoteDCReplicas = new HashMap<>();
+
+ List<Replica> messages = remoteDCReplicas.get(dc);
+ if (messages == null)
+ messages = remoteDCReplicas.computeIfAbsent(dc, ignore -> new ArrayList<>(3)); // most DCs will have <= 3 replicas
+ messages.add(destination);
+ }
+ }
+
+ Preconditions.checkState(applyLocally); // the coordinator is always a replica
+ applyMutationLocally(mutation, handler);
+
+ if (localDCReplicas != null)
+ for (Replica destination : localDCReplicas)
+ MessagingService.instance().sendWriteWithCallback(message, destination, handler);
+
+ if (remoteDCReplicas != null)
+ {
+ // for each datacenter, send the message to one node to relay the write to other replicas
+ for (List<Replica> dcReplicas : remoteDCReplicas.values())
+ sendMessagesToRemoteDC(message, EndpointsForToken.copyOf(mutation.key().getToken(), dcReplicas), handler);
+ }
+ }
+
+ private static void applyMutationLocally(Mutation mutation, TrackedWriteResponseHandler handler)
+ {
+ Stage.MUTATION.maybeExecuteImmediately(new LocalMutationRunnable(mutation, handler));
+ }
+
+ private static class LocalMutationRunnable implements DebuggableTask.RunnableDebuggableTask
+ {
+ private final Mutation mutation;
+ private final TrackedWriteResponseHandler handler;
+
+ LocalMutationRunnable(Mutation mutation, TrackedWriteResponseHandler handler)
+ {
+ this.mutation = mutation;
+ this.handler = handler;
+ }
+
+ @Override
+ public final void run()
+ {
+ long now = MonotonicClock.Global.approxTime.now();
+ long deadline = handler.getRequestTime().computeDeadline(MUTATION_REQ.expiresAfterNanos());
+
+ if (now > deadline)
+ {
+ long timeTakenNanos = now - startTimeNanos();
+ MessagingService.instance().metrics.recordSelfDroppedMessage(Verb.MUTATION_REQ, timeTakenNanos, NANOSECONDS);
+ return;
+ }
+
+ try
+ {
+ mutation.apply();
+ handler.onResponse(null);
+ }
+ catch (Exception ex)
+ {
+ if (!(ex instanceof WriteTimeoutException))
+ logger.error("Failed to apply mutation locally : ", ex);
+ handler.onFailure(FBUtilities.getBroadcastAddressAndPort(), RequestFailureReason.forException(ex));
+ }
+ }
+
+ @Override
+ public long creationTimeNanos()
+ {
+ return handler.getRequestTime().enqueuedAtNanos();
+ }
+
+ @Override
+ public long startTimeNanos()
+ {
+ return handler.getRequestTime().startedAtNanos();
+ }
+
+ @Override
+ public String description()
+ {
+ // description is an Object and toString() called so we do not have to evaluate the Mutation.toString()
+ // unless expliclitly checked
+ return mutation.toString();
+ }
+ }
+
+ /*
+ * Send the message to the first replica of targets, and have it forward the message to others in its DC
+ */
+ private static void sendMessagesToRemoteDC(Message<? extends IMutation> message,
+ EndpointsForToken targets,
+ TrackedWriteResponseHandler handler)
+ {
+ final Replica target;
+
+ if (targets.size() > 1)
+ {
+ target = pickReplica(targets);
+ EndpointsForToken forwardToReplicas = targets.filter(r -> r != target, targets.size());
+
+ for (Replica replica : forwardToReplicas)
+ {
+ MessagingService.instance().callbacks.addWithExpiration(handler, message, replica);
+ logger.trace("Adding FWD message to {}@{}", message.id(), replica);
+ }
+
+ // starting with 4.0, use the same message id for all replicas
+ long[] messageIds = new long[forwardToReplicas.size()];
+ Arrays.fill(messageIds, message.id());
+
+ message = message.withForwardTo(new ForwardingInfo(forwardToReplicas.endpointList(), messageIds));
+ }
+ else
+ {
+ target = targets.get(0);
+ }
+
+ Tracing.trace("Sending mutation to remote replica {}", target);
+ MessagingService.instance().sendWriteWithCallback(message, target, handler);
+ logger.trace("Sending message to {}@{}", message.id(), target);
+ }
+
+ private static Replica pickReplica(EndpointsForToken targets)
+ {
+ EndpointsForToken healthy = targets.filter(r -> DynamicEndpointSnitch.getSeverity(r.endpoint()) == 0);
+ EndpointsForToken select = healthy.isEmpty() ? targets : healthy;
+ return select.get(ThreadLocalRandom.current().nextInt(0, select.size()));
+ }
+}
diff --git a/src/java/org/apache/cassandra/schema/DistributedMetadataLogKeyspace.java b/src/java/org/apache/cassandra/schema/DistributedMetadataLogKeyspace.java
index 82adbb6d80..9bd0378064 100644
--- a/src/java/org/apache/cassandra/schema/DistributedMetadataLogKeyspace.java
+++ b/src/java/org/apache/cassandra/schema/DistributedMetadataLogKeyspace.java
@@ -245,7 +245,7 @@ public final class DistributedMetadataLogKeyspace
public static KeyspaceMetadata initialMetadata(Set<String> knownDatacenters)
{
- return KeyspaceMetadata.create(SchemaConstants.METADATA_KEYSPACE_NAME, new KeyspaceParams(true, ReplicationParams.simpleMeta(1, knownDatacenters), FastPathStrategy.simple()), Tables.of(Log));
+ return KeyspaceMetadata.create(SchemaConstants.METADATA_KEYSPACE_NAME, new KeyspaceParams(true, ReplicationParams.simpleMeta(1, knownDatacenters), FastPathStrategy.simple(), ReplicationType.untracked), Tables.of(Log));
}
public static KeyspaceMetadata initialMetadata(String datacenter)
diff --git a/src/java/org/apache/cassandra/schema/KeyspaceMetadata.java b/src/java/org/apache/cassandra/schema/KeyspaceMetadata.java
index 5239790b19..122c265356 100644
--- a/src/java/org/apache/cassandra/schema/KeyspaceMetadata.java
+++ b/src/java/org/apache/cassandra/schema/KeyspaceMetadata.java
@@ -120,27 +120,27 @@ public final class KeyspaceMetadata implements SchemaElement
public static KeyspaceMetadata create(String name, KeyspaceParams params, Tables tables)
{
- return new KeyspaceMetadata(name, Kind.REGULAR, params, tables, Views.none(), Types.none(), UserFunctions.none());
+ return new KeyspaceMetadata(name, Kind.REGULAR, params, tables.withKeyspaceReplicationType(params.replicationType), Views.none(), Types.none(), UserFunctions.none());
}
public static KeyspaceMetadata create(String name, KeyspaceParams params, Tables tables, Views views, Types types, UserFunctions functions)
{
- return new KeyspaceMetadata(name, Kind.REGULAR, params, tables, views, types, functions);
+ return new KeyspaceMetadata(name, Kind.REGULAR, params, tables.withKeyspaceReplicationType(params.replicationType), views, types, functions);
}
public static KeyspaceMetadata virtual(String name, Tables tables)
{
- return new KeyspaceMetadata(name, Kind.VIRTUAL, KeyspaceParams.local(), tables, Views.none(), Types.none(), UserFunctions.none());
+ return new KeyspaceMetadata(name, Kind.VIRTUAL, KeyspaceParams.local(), tables.withKeyspaceReplicationType(ReplicationType.untracked), Views.none(), Types.none(), UserFunctions.none());
}
public KeyspaceMetadata withSwapped(KeyspaceParams params)
{
- return new KeyspaceMetadata(name, kind, params, tables, views, types, userFunctions);
+ return new KeyspaceMetadata(name, kind, params, tables.withKeyspaceReplicationType(params.replicationType), views, types, userFunctions);
}
public KeyspaceMetadata withSwapped(Tables regular)
{
- return new KeyspaceMetadata(name, kind, params, regular, views, types, userFunctions);
+ return new KeyspaceMetadata(name, kind, params, regular.withKeyspaceReplicationType(params.replicationType), views, types, userFunctions);
}
public KeyspaceMetadata withSwapped(Views views)
@@ -290,6 +290,11 @@ public final class KeyspaceMetadata implements SchemaElement
return Optional.empty();
}
+ public boolean useMutationTracking()
+ {
+ return params.replicationType.isTracked();
+ }
+
@Override
public int hashCode()
{
@@ -384,7 +389,9 @@ public final class KeyspaceMetadata implements SchemaElement
params.replication.appendCqlTo(builder);
builder.append(" AND durable_writes = ")
- .append(params.durableWrites);
+ .append(params.durableWrites)
+ .append(" AND replication_type = ")
+ .appendWithSingleQuotes(params.replicationType.name());
if (params.fastPath != null)
{
diff --git a/src/java/org/apache/cassandra/schema/KeyspaceParams.java b/src/java/org/apache/cassandra/schema/KeyspaceParams.java
index 4f41235367..38b203dee3 100644
--- a/src/java/org/apache/cassandra/schema/KeyspaceParams.java
+++ b/src/java/org/apache/cassandra/schema/KeyspaceParams.java
@@ -55,11 +55,14 @@ public final class KeyspaceParams
@VisibleForTesting
public static boolean DEFAULT_LOCAL_DURABLE_WRITES = true;
+ public static final ReplicationType DEFAULT_REPLICATION_TYPE = ReplicationType.untracked;
+
public enum Option
{
DURABLE_WRITES,
REPLICATION,
- FAST_PATH;
+ FAST_PATH,
+ REPLICATION_TYPE;
@Override
public String toString()
@@ -73,74 +76,97 @@ public final class KeyspaceParams
public final FastPathStrategy fastPath;
public final String comment;
public final String securityLabel;
+ public final ReplicationType replicationType;
- public KeyspaceParams(boolean durableWrites, ReplicationParams replication, FastPathStrategy fastPath)
+ public KeyspaceParams(boolean durableWrites, ReplicationParams replication, FastPathStrategy fastPath, ReplicationType replicationType)
{
- this(durableWrites, replication, fastPath, EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
+ this(durableWrites, replication, fastPath, EMPTY_COMMENT, EMPTY_SECURITY_LABEL, replicationType);
}
- public KeyspaceParams(boolean durableWrites, ReplicationParams replication, FastPathStrategy fastPath, String comment, String securityLabel)
+ public KeyspaceParams(boolean durableWrites, ReplicationParams replication, FastPathStrategy fastPath, String comment, String securityLabel, ReplicationType replicationType)
{
this.durableWrites = durableWrites;
this.replication = replication;
this.fastPath = fastPath;
this.comment = comment == null ? EMPTY_COMMENT : comment;
this.securityLabel = securityLabel == null ? EMPTY_SECURITY_LABEL : securityLabel;
+ this.replicationType = replicationType;
+ }
+
+ public static KeyspaceParams create(boolean durableWrites, Map<String, String> replication, FastPathStrategy fastPath, ReplicationType replicationType)
+ {
+ return new KeyspaceParams(durableWrites, ReplicationParams.fromMap(replication), fastPath, EMPTY_COMMENT, EMPTY_SECURITY_LABEL, replicationType);
}
- public static KeyspaceParams create(boolean durableWrites, Map<String, String> replication, FastPathStrategy fastPath)
+ public static KeyspaceParams create(boolean durableWrites, Map<String, String> replication, ReplicationType replicationType)
{
- return new KeyspaceParams(durableWrites, ReplicationParams.fromMap(replication), fastPath, EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
+ return create(durableWrites, replication, FastPathStrategy.simple(), replicationType);
}
public static KeyspaceParams create(boolean durableWrites, Map<String, String> replication, Map<String, String> fastPath)
{
- return create(durableWrites, replication, FastPathStrategy.fromMap(fastPath));
+ return create(durableWrites, replication, FastPathStrategy.fromMap(fastPath), ReplicationType.untracked);
+ }
+
+ public static KeyspaceParams create(boolean durableWrites, Map<String, String> replication, Map<String, String> fastPath, ReplicationType replicationType)
+ {
+ return create(durableWrites, replication, FastPathStrategy.fromMap(fastPath), replicationType);
}
public static KeyspaceParams create(boolean durableWrites, Map<String, String> replication)
{
- return create(durableWrites, replication, FastPathStrategy.simple());
+ return create(durableWrites, replication, FastPathStrategy.simple(), ReplicationType.untracked);
}
public static KeyspaceParams local()
{
- return new KeyspaceParams(DEFAULT_LOCAL_DURABLE_WRITES, ReplicationParams.local(), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
+ return new KeyspaceParams(DEFAULT_LOCAL_DURABLE_WRITES, ReplicationParams.local(), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL, ReplicationType.untracked);
+ }
+
+ public static KeyspaceParams simple(int replicationFactor, ReplicationType replicationType)
+ {
+ return new KeyspaceParams(true, ReplicationParams.simple(replicationFactor), FastPathStrategy.simple(), replicationType);
}
public static KeyspaceParams simple(int replicationFactor)
{
- return new KeyspaceParams(true, ReplicationParams.simple(replicationFactor), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
+ return new KeyspaceParams(true, ReplicationParams.simple(replicationFactor), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL, ReplicationType.untracked);
}
public static KeyspaceParams simple(String replicationFactor)
{
- return new KeyspaceParams(true, ReplicationParams.simple(replicationFactor), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
+ return new KeyspaceParams(true, ReplicationParams.simple(replicationFactor), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL, ReplicationType.untracked);
}
public static KeyspaceParams simpleTransient(int replicationFactor)
{
- return new KeyspaceParams(false, ReplicationParams.simple(replicationFactor), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
+ return new KeyspaceParams(false, ReplicationParams.simple(replicationFactor), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL, ReplicationType.untracked);
}
+ public static KeyspaceParams nts(ReplicationType replicationType, Object... args)
+ {
+ return new KeyspaceParams(true, ReplicationParams.nts(args), FastPathStrategy.simple(), replicationType);
+ }
+
+
public static KeyspaceParams nts(Object... args)
{
- return new KeyspaceParams(true, ReplicationParams.nts(args), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
+ return nts(ReplicationType.untracked, args);
}
public KeyspaceParams withSwapped(ReplicationParams params)
{
- return new KeyspaceParams(durableWrites, params, fastPath, comment, securityLabel);
+ return new KeyspaceParams(durableWrites, params, fastPath, comment, securityLabel, replicationType);
}
public KeyspaceParams withComment(String comment)
{
- return new KeyspaceParams(durableWrites, replication, fastPath, comment, securityLabel);
+ return new KeyspaceParams(durableWrites, replication, fastPath, comment, securityLabel, replicationType);
}
public KeyspaceParams withSecurityLabel(String securityLabel)
{
- return new KeyspaceParams(durableWrites, replication, fastPath, comment, securityLabel);
+ return new KeyspaceParams(durableWrites, replication, fastPath, comment, securityLabel, replicationType);
}
public void validate(String name, ClientState state, ClusterMetadata metadata)
@@ -163,13 +189,14 @@ public final class KeyspaceParams
&& replication.equals(p.replication)
&& fastPath.equals(p.fastPath)
&& comment.equals(p.comment)
- && securityLabel.equals(p.securityLabel);
+ && securityLabel.equals(p.securityLabel)
+ && replicationType == p.replicationType;
}
@Override
public int hashCode()
{
- return Objects.hashCode(durableWrites, replication, fastPath, comment, securityLabel);
+ return Objects.hashCode(durableWrites, replication, fastPath, comment, securityLabel, replicationType);
}
@Override
@@ -181,6 +208,7 @@ public final class KeyspaceParams
.add(Option.FAST_PATH.toString(), fastPath.toString())
.add("COMMENT", comment)
.add("SECURITY_LABEL", securityLabel)
+ .add(Option.REPLICATION_TYPE.toString(), replicationType)
.toString();
}
@@ -188,6 +216,7 @@ public final class KeyspaceParams
{
public void serialize(KeyspaceParams t, DataOutputPlus out, Version version) throws IOException
{
+ ReplicationType.serializer.serialize(t.replicationType, out, version);
ReplicationParams.serializer.serialize(t.replication, out, version);
out.writeBoolean(t.durableWrites);
if (version.isAtLeast(MIN_ACCORD_VERSION))
@@ -201,6 +230,7 @@ public final class KeyspaceParams
public KeyspaceParams deserialize(DataInputPlus in, Version version) throws IOException
{
+ ReplicationType rtype = ReplicationType.serializer.deserialize(in, version);
ReplicationParams params = ReplicationParams.serializer.deserialize(in, version);
boolean durableWrites = in.readBoolean();
FastPathStrategy fastPath = version.isAtLeast(MIN_ACCORD_VERSION)
@@ -213,12 +243,13 @@ public final class KeyspaceParams
comment = in.readUTF();
securityLabel = in.readUTF();
}
- return new KeyspaceParams(durableWrites, params, fastPath, comment, securityLabel);
+ return new KeyspaceParams(durableWrites, params, fastPath, comment, securityLabel, rtype);
}
public long serializedSize(KeyspaceParams t, Version version)
{
- return ReplicationParams.serializer.serializedSize(t.replication, version) +
+ return ReplicationType.serializer.serializedSize(t.replicationType, version) +
+ ReplicationParams.serializer.serializedSize(t.replication, version) +
TypeSizes.sizeof(t.durableWrites) +
(version.isAtLeast(Version.V8) ? TypeSizes.sizeof(t.comment) : 0) +
(version.isAtLeast(Version.V8) ? TypeSizes.sizeof(t.securityLabel) : 0) +
diff --git a/src/java/org/apache/cassandra/schema/ReplicationType.java b/src/java/org/apache/cassandra/schema/ReplicationType.java
new file mode 100644
index 0000000000..575e9b7f78
--- /dev/null
+++ b/src/java/org/apache/cassandra/schema/ReplicationType.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.schema;
+
+import java.io.IOException;
+
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.tcm.serialization.MetadataSerializer;
+import org.apache.cassandra.tcm.serialization.Version;
+
+public enum ReplicationType
+{
+ untracked, tracked;
+
+ public static final MetadataSerializer<ReplicationType> serializer = new MetadataSerializer<>()
+ {
+ @Override
+ public void serialize(ReplicationType t, DataOutputPlus out, Version version) throws IOException
+ {
+ if (version.isBefore(Version.V9))
+ return;
+
+ switch (t)
+ {
+ case untracked:
+ out.writeByte(0);
+ break;
+ case tracked:
+ out.writeByte(1);
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported replication type: " + t);
+ }
+ }
+
+ @Override
+ public ReplicationType deserialize(DataInputPlus in, Version version) throws IOException
+ {
+ if (version.isBefore(Version.V9))
+ return untracked;
+
+ byte t = in.readByte();
+
+ switch (t)
+ {
+ case 0:
+ return untracked;
+ case 1:
+ return tracked;
+ default:
+ throw new IllegalArgumentException("Unsupported replication type: " + t);
+ }
+ }
+
+ @Override
+ public long serializedSize(ReplicationType t, Version version)
+ {
+ if (version.isBefore(Version.V9))
+ return 0;
+ return TypeSizes.BYTE_SIZE;
+ }
+ };
+
+ public boolean isTracked()
+ {
+ return this == tracked;
+ }
+
+ // FIXME: used in lieu of adding support for tracked reads in parameterized tests, fix usages of this method
+ public static ReplicationType[] fixmeValues()
+ {
+ return new ReplicationType[]{ untracked };
+ }
+}
diff --git a/src/java/org/apache/cassandra/schema/SchemaKeyspace.java b/src/java/org/apache/cassandra/schema/SchemaKeyspace.java
index f8057ed8b7..a0934755ee 100644
--- a/src/java/org/apache/cassandra/schema/SchemaKeyspace.java
+++ b/src/java/org/apache/cassandra/schema/SchemaKeyspace.java
@@ -82,6 +82,7 @@ import org.apache.cassandra.db.rows.RowIterator;
import org.apache.cassandra.db.rows.RowIterators;
import org.apache.cassandra.db.rows.UnfilteredRowIterator;
import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.schema.ColumnMetadata.ClusteringOrder;
import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff;
import org.apache.cassandra.service.accord.fastpath.FastPathStrategy;
@@ -145,6 +146,7 @@ public final class SchemaKeyspace
"CREATE TABLE %s ("
+ "keyspace_name text,"
+ "durable_writes boolean,"
+ + "replication_type text,"
+ "replication frozen<map<text, text>>,"
+ "fast_path frozen<map<text, text>>,"
+ "PRIMARY KEY ((keyspace_name)))");
@@ -488,7 +490,7 @@ public final class SchemaKeyspace
continue;
DecoratedKey key = partition.partitionKey();
- Mutation.PartitionUpdateCollector puCollector = mutationMap.computeIfAbsent(key, k -> new Mutation.PartitionUpdateCollector(SchemaConstants.SCHEMA_KEYSPACE_NAME, key));
+ Mutation.PartitionUpdateCollector puCollector = mutationMap.computeIfAbsent(key, k -> new Mutation.PartitionUpdateCollector(MutationId.none(), SchemaConstants.SCHEMA_KEYSPACE_NAME, key));
puCollector.add(makeUpdateForSchema(partition, cmd.columnFilter()).withOnlyPresentColumns());
}
}
@@ -537,12 +539,13 @@ public final class SchemaKeyspace
private static Mutation.SimpleBuilder makeCreateKeyspaceMutation(String name, KeyspaceParams params, long timestamp)
{
- Mutation.SimpleBuilder builder = Mutation.simpleBuilder(Keyspaces.keyspace, decorate(Keyspaces, name))
+ Mutation.SimpleBuilder builder = Mutation.simpleBuilder(MutationId.none(), Keyspaces.keyspace, decorate(Keyspaces, name))
.timestamp(timestamp);
builder.update(Keyspaces)
.row()
.add(KeyspaceParams.Option.DURABLE_WRITES.toString(), params.durableWrites)
+ .add(KeyspaceParams.Option.REPLICATION_TYPE.toString(), params.replicationType.toString())
.add(KeyspaceParams.Option.REPLICATION.toString(),
(params.replication.isMeta() ? params.replication.asNonMeta() : params.replication).asMap())
.add(KeyspaceParams.Option.FAST_PATH.toString(), params.fastPath.asMap());
@@ -566,7 +569,7 @@ public final class SchemaKeyspace
private static Mutation.SimpleBuilder makeDropKeyspaceMutation(KeyspaceMetadata keyspace, long timestamp)
{
- Mutation.SimpleBuilder builder = Mutation.simpleBuilder(SchemaConstants.SCHEMA_KEYSPACE_NAME, decorate(Keyspaces, keyspace.name))
+ Mutation.SimpleBuilder builder = Mutation.simpleBuilder(MutationId.none(), SchemaConstants.SCHEMA_KEYSPACE_NAME, decorate(Keyspaces, keyspace.name))
.timestamp(timestamp);
for (TableMetadata schemaTable : ALL_TABLE_METADATA)
@@ -1030,12 +1033,24 @@ public final class SchemaKeyspace
UntypedResultSet.Row row = query(query, keyspaceName).one();
boolean durableWrites = row.getBoolean(KeyspaceParams.Option.DURABLE_WRITES.toString());
+
+ ReplicationType replicationType;
+ if (row.has(KeyspaceParams.Option.REPLICATION_TYPE.toString()))
+ {
+ String replicationTypeName = row.getString(KeyspaceParams.Option.REPLICATION_TYPE.toString());
+ replicationType = replicationTypeName != null ? ReplicationType.valueOf(replicationTypeName) : ReplicationType.untracked;
+ }
+ else
+ {
+ replicationType = ReplicationType.untracked;
+ }
+
Map<String, String> replication = row.getFrozenTextMap(KeyspaceParams.Option.REPLICATION.toString());
Map<String, String> fastPath = row.getFrozenTextMap(KeyspaceParams.Option.FAST_PATH.toString());
- KeyspaceParams params = KeyspaceParams.create(durableWrites, replication, fastPath);
+ KeyspaceParams params = KeyspaceParams.create(durableWrites, replication, fastPath, replicationType);
if (keyspaceName.equals(SchemaConstants.METADATA_KEYSPACE_NAME))
- params = new KeyspaceParams(params.durableWrites, params.replication.asMeta(), FastPathStrategy.simple());
+ params = new KeyspaceParams(params.durableWrites, params.replication.asMeta(), FastPathStrategy.simple(), ReplicationType.untracked);
return params;
}
diff --git a/src/java/org/apache/cassandra/schema/TableMetadata.java b/src/java/org/apache/cassandra/schema/TableMetadata.java
index 9589062c8b..a22ccfebb1 100644
--- a/src/java/org/apache/cassandra/schema/TableMetadata.java
+++ b/src/java/org/apache/cassandra/schema/TableMetadata.java
@@ -185,6 +185,7 @@ public class TableMetadata implements SchemaElement
public final IPartitioner partitioner;
public final Kind kind;
public final TableParams params;
+ public final ReplicationType keyspaceReplicationType;
public final ImmutableSet<Flag> flags;
@Nullable
@@ -233,6 +234,7 @@ public class TableMetadata implements SchemaElement
partitioner = builder.partitioner;
kind = builder.kind;
params = builder.params.build();
+ keyspaceReplicationType = builder.keyspaceReplicationType;
indexName = kind == Kind.INDEX ? name.substring(name.indexOf('.') + 1) : null;
@@ -316,6 +318,7 @@ public class TableMetadata implements SchemaElement
.partitioner(partitioner)
.kind(kind)
.params(params)
+ .keyspaceReplicationType(keyspaceReplicationType)
.flags(flags)
.addColumns(columns())
.droppedColumns(droppedColumns)
@@ -329,6 +332,11 @@ public class TableMetadata implements SchemaElement
return kind == Kind.INDEX;
}
+ public TableMetadata withKeyspaceReplicationType(ReplicationType type)
+ {
+ return unbuild().keyspaceReplicationType(type).build();
+ }
+
public TableMetadata withSwapped(TableParams params)
{
return unbuild().params(params).build();
@@ -364,6 +372,13 @@ public class TableMetadata implements SchemaElement
return kind == Kind.VIRTUAL;
}
+ public ReplicationType replicationType()
+ {
+ if (kind != Kind.REGULAR)
+ return ReplicationType.untracked;
+ return keyspaceReplicationType;
+ }
+
public Optional<String> indexName()
{
return Optional.ofNullable(indexName);
@@ -975,6 +990,7 @@ public class TableMetadata implements SchemaElement
private IPartitioner partitioner;
private Kind kind = Kind.REGULAR;
private TableParams.Builder params = TableParams.builder();
+ private ReplicationType keyspaceReplicationType = ReplicationType.untracked;
// See the comment on Flag.COMPOUND definition for why we (still) inconditionally add this flag.
private Set<Flag> flags = EnumSet.of(Flag.COMPOUND);
@@ -1077,6 +1093,12 @@ public class TableMetadata implements SchemaElement
return new ColumnMetadata(prev.ksName, prev.cfName, prev.name, prev.type, uniqueId, prev.position(), prev.kind, prev.getMask(), prev.getColumnConstraints(), prev.comment, prev.securityLabel);
}
+ public Builder keyspaceReplicationType(ReplicationType type)
+ {
+ keyspaceReplicationType = type;
+ return this;
+ }
+
public Builder id(TableId val)
{
id = val;
@@ -1102,6 +1124,8 @@ public class TableMetadata implements SchemaElement
public Builder kind(Kind val)
{
+ if (val != Kind.REGULAR)
+ keyspaceReplicationType = null;
kind = val;
return this;
}
diff --git a/src/java/org/apache/cassandra/schema/Tables.java b/src/java/org/apache/cassandra/schema/Tables.java
index f0e7ad03e5..9a576f6c49 100644
--- a/src/java/org/apache/cassandra/schema/Tables.java
+++ b/src/java/org/apache/cassandra/schema/Tables.java
@@ -192,6 +192,13 @@ public final class Tables implements Iterable<TableMetadata>
: this;
}
+ public Tables withKeyspaceReplicationType(ReplicationType type)
+ {
+ return any(this, t -> t.keyspaceReplicationType != type)
+ ? builder().add(transform(this, t -> t.withKeyspaceReplicationType(type))).build()
+ : this;
+ }
+
MapDifference<String, TableMetadata> indexesDiff(Tables other)
{
Map<String, TableMetadata> thisIndexTables = new HashMap<>();
diff --git a/src/java/org/apache/cassandra/service/CassandraDaemon.java b/src/java/org/apache/cassandra/service/CassandraDaemon.java
index 4a5c2f1d9d..9a40915a60 100644
--- a/src/java/org/apache/cassandra/service/CassandraDaemon.java
+++ b/src/java/org/apache/cassandra/service/CassandraDaemon.java
@@ -80,6 +80,7 @@ import org.apache.cassandra.io.util.File;
import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.locator.Locator;
+import org.apache.cassandra.replication.MutationJournal;
import org.apache.cassandra.metrics.CassandraMetricsRegistry;
import org.apache.cassandra.metrics.DefaultNameFactory;
import org.apache.cassandra.net.StartupClusterConnectivityChecker;
@@ -271,6 +272,7 @@ public class CassandraDaemon
Keyspace.setInitialized();
CommitLog.instance.start();
+ MutationJournal.instance.start();
SnapshotManager.instance.start(false);
SnapshotManager.instance.clearExpiredSnapshots();
diff --git a/src/java/org/apache/cassandra/service/StorageProxy.java b/src/java/org/apache/cassandra/service/StorageProxy.java
index cc97da02aa..78f67ced46 100644
--- a/src/java/org/apache/cassandra/service/StorageProxy.java
+++ b/src/java/org/apache/cassandra/service/StorageProxy.java
@@ -134,6 +134,7 @@ import org.apache.cassandra.net.MessageFlag;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.RequestCallback;
import org.apache.cassandra.net.Verb;
+import org.apache.cassandra.replication.TrackedWriteRequest;
import org.apache.cassandra.schema.PartitionDenylist;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.SchemaConstants;
@@ -173,6 +174,7 @@ import org.apache.cassandra.service.reads.ReadCallback;
import org.apache.cassandra.service.reads.ReadCoordinator;
import org.apache.cassandra.service.reads.range.RangeCommands;
import org.apache.cassandra.service.reads.repair.ReadRepair;
+import org.apache.cassandra.service.reads.tracked.TrackedRead;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.membership.NodeState;
import org.apache.cassandra.tcm.ownership.VersionedEndpoints;
@@ -194,6 +196,7 @@ import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
import static accord.primitives.Txn.Kind.Read;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.concat;
+import static java.util.Collections.singleton;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static org.apache.cassandra.db.ConsistencyLevel.SERIAL;
@@ -961,6 +964,71 @@ public class StorageProxy implements StorageProxyMBean
});
}
+ /**
+ * Performs a coordinated write with mutation tracking.
+ * Assumes that local coordinator is a replica (forwarding implementation pending).
+ *
+ * @param mutation
+ * @param consistencyLevel
+ * @param requestTime
+ */
+ public static void mutateWithTracking(Mutation mutation, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime)
+ {
+ try
+ {
+ TrackedWriteRequest.perform(mutation, consistencyLevel, requestTime).get();
+ }
+ catch (WriteTimeoutException|WriteFailureException ex)
+ {
+ if (consistencyLevel == ConsistencyLevel.ANY)
+ {
+ // TODO (expected): what exactly?
+ }
+ else
+ {
+ if (ex instanceof WriteFailureException)
+ {
+ writeMetrics.failures.mark();
+ writeMetricsForLevel(consistencyLevel).failures.mark();
+ WriteFailureException fe = (WriteFailureException)ex;
+ Tracing.trace("Write failure; received {} of {} required replies, failed {} requests",
+ fe.received, fe.blockFor, fe.failureReasonByEndpoint.size());
+ }
+ else
+ {
+ writeMetrics.timeouts.mark();
+ writeMetricsForLevel(consistencyLevel).timeouts.mark();
+ WriteTimeoutException te = (WriteTimeoutException)ex;
+ Tracing.trace("Write timeout; received {} of {} required replies", te.received, te.blockFor);
+ }
+ throw ex;
+ }
+ }
+ catch (UnavailableException e)
+ {
+ writeMetrics.unavailables.mark();
+ writeMetricsForLevel(consistencyLevel).unavailables.mark();
+ Tracing.trace("Unavailable");
+ throw e;
+ }
+ catch (OverloadedException e)
+ {
+ writeMetrics.unavailables.mark();
+ writeMetricsForLevel(consistencyLevel).unavailables.mark();
+ Tracing.trace("Overloaded");
+ throw e;
+ }
+ finally
+ {
+ // We track latency based on request processing time, since the amount of time that request spends in the queue
+ // is not a representative metric of replica performance.
+ long latency = nanoTime() - requestTime.startedAtNanos();
+ writeMetrics.addNano(latency);
+ writeMetricsForLevel(consistencyLevel).addNano(latency);
+ updateCoordinatorWriteLatencyTableMetric(singleton(mutation), latency);
+ }
+ }
+
/**
* Use this method to have these Mutations applied
* across all replicas. This method will take care
@@ -1220,11 +1288,11 @@ public class StorageProxy implements StorageProxyMBean
}
@SuppressWarnings("unchecked")
- public static void mutateWithTriggers(List<? extends IMutation> mutations,
- ConsistencyLevel consistencyLevel,
- boolean mutateAtomically,
- Dispatcher.RequestTime requestTime,
- PreserveTimestamp preserveTimestamps)
+ public static void mutateWithoutConditions(List<? extends IMutation> mutations,
+ ConsistencyLevel consistencyLevel,
+ boolean mutateAtomically,
+ Dispatcher.RequestTime requestTime,
+ PreserveTimestamp preserveTimestamps)
throws WriteTimeoutException, WriteFailureException, UnavailableException, OverloadedException, InvalidRequestException
{
if (DatabaseDescriptor.getPartitionDenylistEnabled() && DatabaseDescriptor.getDenylistWritesEnabled())
@@ -1256,7 +1324,24 @@ public class StorageProxy implements StorageProxyMBean
long size = IMutation.dataSize(augmented != null ? augmented : mutations);
writeMetrics.mutationSize.update(size);
writeMetricsForLevel(consistencyLevel).mutationSize.update(size);
- if (augmented != null || mutateAtomically || updatesView)
+
+ boolean isTracked = Schema.instance.getKeyspaceMetadata(mutations.get(0).getKeyspaceName()).params.replicationType.isTracked();
+ if (isTracked)
+ {
+ if (mutations.stream().anyMatch(m -> m instanceof CounterMutation))
+ throw new InvalidRequestException("Mutation tracking is currently unsupported with counters");
+ if (augmented != null)
+ throw new InvalidRequestException("Mutation tracking is currently unsupported with triggers");
+ if (mutateAtomically)
+ throw new InvalidRequestException("Mutation tracking is currently unsupported with logged batches");
+ if (mutations.size() > 1)
+ throw new InvalidRequestException("Mutation tracking is currently unsupported with unlogged batches");
+ if (updatesView)
+ throw new InvalidRequestException("Mutation tracking is currently unsupported with materialized views");
+
+ mutateWithTracking((Mutation) mutations.get(0), consistencyLevel, requestTime);
+ }
+ else if (augmented != null || mutateAtomically || updatesView)
mutateAtomically(augmented != null ? augmented : (List<Mutation>)mutations, consistencyLevel, updatesView, requestTime);
else
dispatchMutationsWithRetryOnDifferentSystem(mutations, consistencyLevel, requestTime, preserveTimestamps);
@@ -2628,6 +2713,30 @@ public class StorageProxy implements StorageProxyMBean
};
}
+ private static PartitionIterator fetchRowsTracked(List<SinglePartitionReadCommand> commands,
+ ConsistencyLevel consistencyLevel,
+ Dispatcher.RequestTime requestTime)
+ {
+ int cmdCount = commands.size();
+ TrackedRead.Partition[] reads = new TrackedRead.Partition[cmdCount];
+ ClusterMetadata metadata = ClusterMetadata.current();
+
+ for (int i=0; i<cmdCount; i++)
+ reads[i] = TrackedRead.Partition.create(metadata, commands.get(i), consistencyLevel);
+
+ for (TrackedRead.Partition read : reads)
+ read.start(requestTime);
+
+ if (cmdCount == 1)
+ return reads[0].awaitResults();
+
+ List<PartitionIterator> iterators = new ArrayList<>(cmdCount);
+ for (TrackedRead.Partition read : reads)
+ iterators.add(read.awaitResults());
+
+ return PartitionIterators.concat(iterators);
+ }
+
/**
* This function executes local and remote reads, and blocks for the results:
*
@@ -2642,10 +2751,10 @@ public class StorageProxy implements StorageProxyMBean
* This should not be called directly because it bypasses statistics and error handling. It is public
* so it can be used by Accord to fetch rows and the statistics will be tracked by Accord.
*/
- public static PartitionIterator fetchRows(List<SinglePartitionReadCommand> commands,
- ConsistencyLevel consistencyLevel,
- ReadCoordinator coordinator,
- Dispatcher.RequestTime requestTime)
+ public static PartitionIterator fetchRowsUntracked(List<SinglePartitionReadCommand> commands,
+ ConsistencyLevel consistencyLevel,
+ ReadCoordinator coordinator,
+ Dispatcher.RequestTime requestTime)
throws UnavailableException, ReadFailureException, ReadTimeoutException
{
int cmdCount = commands.size();
@@ -2714,6 +2823,22 @@ public class StorageProxy implements StorageProxyMBean
return concatAndBlockOnRepair(results, repairs);
}
+ private static PartitionIterator fetchRows(List<SinglePartitionReadCommand> commands,
+ ConsistencyLevel consistencyLevel,
+ ReadCoordinator coordinator,
+ Dispatcher.RequestTime requestTime)
+ {
+ if (commands.get(0).metadata().replicationType().isTracked())
+ {
+ return fetchRowsTracked(commands, consistencyLevel, requestTime);
+ }
+ else
+ {
+ return fetchRowsUntracked(commands, consistencyLevel, coordinator, requestTime);
+ }
+
+ }
+
public static class LocalReadRunnable extends DroppableRunnable implements RunnableDebuggableTask
{
private final ReadCommand command;
diff --git a/src/java/org/apache/cassandra/service/StorageService.java b/src/java/org/apache/cassandra/service/StorageService.java
index 424df75b97..1b953d7f96 100644
--- a/src/java/org/apache/cassandra/service/StorageService.java
+++ b/src/java/org/apache/cassandra/service/StorageService.java
@@ -165,6 +165,8 @@ import org.apache.cassandra.repair.RepairParallelism;
import org.apache.cassandra.repair.SharedContext;
import org.apache.cassandra.repair.autorepair.AutoRepair;
import org.apache.cassandra.repair.messages.RepairOption;
+import org.apache.cassandra.replication.MutationJournal;
+import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.schema.CompactionParams.TombstoneOption;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.Keyspaces;
@@ -3963,6 +3965,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
SnapshotManager.instance.shutdownAndWait(1L, MINUTES);
HintsService.instance.shutdownBlocking();
+ MutationTrackingService.instance.shutdownBlocking();
// Interrupt ongoing compactions and shutdown CM to prevent further compactions.
CompactionManager.instance.forceShutdown();
@@ -3972,6 +3975,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
CommitLog.instance.forceRecycleAllSegments();
CommitLog.instance.shutdownBlocking();
+ MutationJournal.instance.shutdownBlocking();
AutoRepair.instance.shutdownBlocking();
diff --git a/src/java/org/apache/cassandra/service/TrackedWriteResponseHandler.java b/src/java/org/apache/cassandra/service/TrackedWriteResponseHandler.java
new file mode 100644
index 0000000000..b224cbf1c0
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/TrackedWriteResponseHandler.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.service;
+
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.exceptions.RequestFailureReason;
+import org.apache.cassandra.exceptions.WriteFailureException;
+import org.apache.cassandra.exceptions.WriteTimeoutException;
+import org.apache.cassandra.locator.InetAddressAndPort;
+import org.apache.cassandra.net.Message;
+import org.apache.cassandra.net.NoPayload;
+import org.apache.cassandra.replication.MutationId;
+import org.apache.cassandra.replication.MutationTrackingService;
+
+public class TrackedWriteResponseHandler extends AbstractWriteResponseHandler<NoPayload>
+{
+ private final AbstractWriteResponseHandler<NoPayload> wrapped;
+
+ private final String keyspace;
+ private final Token token;
+ private final MutationId mutationId;
+
+ private TrackedWriteResponseHandler(
+ AbstractWriteResponseHandler<NoPayload> wrapped, String keyspace, Token token, MutationId mutationId)
+ {
+ super(wrapped.replicaPlan, wrapped.callback, wrapped.writeType, null, wrapped.getRequestTime());
+ this.wrapped = wrapped;
+ this.keyspace = keyspace;
+ this.token = token;
+ this.mutationId = mutationId;
+ }
+
+ public static TrackedWriteResponseHandler wrap(
+ AbstractWriteResponseHandler<NoPayload> handler, String keyspace, Token token, MutationId mutationId)
+ {
+ return new TrackedWriteResponseHandler(handler, keyspace, token, mutationId);
+ }
+
+ @Override
+ public void onResponse(Message<NoPayload> msg)
+ {
+ /* local mutations are witnessed from Keyspace.applyInternalTracked */
+ if (msg != null)
+ MutationTrackingService.instance.witnessedRemoteMutation(keyspace, token, mutationId, msg.from());
+
+ wrapped.onResponse(msg);
+ }
+
+ @Override
+ public void onFailure(InetAddressAndPort from, RequestFailureReason failureReason)
+ {
+ wrapped.onFailure(from, failureReason);
+ }
+
+ @Override
+ public boolean trackLatencyForSnitch()
+ {
+ return wrapped.trackLatencyForSnitch();
+ }
+
+ @Override
+ protected int ackCount()
+ {
+ return wrapped.ackCount();
+ }
+
+ @Override
+ public boolean invokeOnFailure()
+ {
+ return wrapped.invokeOnFailure();
+ }
+
+ @Override
+ public void get() throws WriteTimeoutException, WriteFailureException
+ {
+ wrapped.get();
+ }
+
+ @Override
+ protected int blockFor()
+ {
+ return wrapped.blockFor();
+ }
+
+ @Override
+ protected int candidateReplicaCount()
+ {
+ return wrapped.candidateReplicaCount();
+ }
+
+ @Override
+ protected boolean waitingFor(InetAddressAndPort from)
+ {
+ return wrapped.waitingFor(from);
+ }
+
+ @Override
+ protected void signal()
+ {
+ wrapped.signal();
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/paxos/Commit.java b/src/java/org/apache/cassandra/service/paxos/Commit.java
index fd4102e038..896beabdf2 100644
--- a/src/java/org/apache/cassandra/service/paxos/Commit.java
+++ b/src/java/org/apache/cassandra/service/paxos/Commit.java
@@ -36,6 +36,7 @@ import org.apache.cassandra.db.rows.DeserializationHelper;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.schema.TableMetadata;
import static org.apache.cassandra.db.SystemKeyspace.legacyPaxosTtlSec;
@@ -316,7 +317,15 @@ public class Commit
public Mutation makeMutation()
{
- return new Mutation(update, PotentialTxnConflicts.ALLOW);
+ // TODO (expected): what's the best thing to do here? Deriving the mutation id from the ballot seems like the best
+ // thing to do, like we do with the partition update timestamps, but there are caveats related to id collisions
+ // and the assumption that a mutation id is unique amonth other mutations, which is not the case w/ paxos ballots,
+ // which only need to be unique to a given partition key to be accepted. It may be best to keep them separate anyway,
+ // since the reconciliation process as currently planned will not allow writes with ids before some point in time,
+ // and there might be edge cases where that locks up paxos execution or has other side effects. The downside of
+ // not making paxos mutation ids deterministic is that the same commit may create multiple mutation ids if a paxos
+ // operation is not fully committed, then re-committed on repair or the next operation
+ return new Mutation(MutationId.fixme(), update, PotentialTxnConflicts.ALLOW);
}
@Override
diff --git a/src/java/org/apache/cassandra/service/reads/AbstractReadExecutor.java b/src/java/org/apache/cassandra/service/reads/AbstractReadExecutor.java
index 814aa74481..f73c33de5c 100644
--- a/src/java/org/apache/cassandra/service/reads/AbstractReadExecutor.java
+++ b/src/java/org/apache/cassandra/service/reads/AbstractReadExecutor.java
@@ -199,6 +199,7 @@ public abstract class AbstractReadExecutor
ReadCoordinator coordinator,
Dispatcher.RequestTime requestTime) throws UnavailableException
{
+ Preconditions.checkArgument(!command.metadata().replicationType().isTracked());
Keyspace keyspace = Keyspace.open(command.metadata().keyspace);
ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(command.metadata().id);
SpeculativeRetryPolicy retry = cfs.metadata().params.speculativeRetry;
diff --git a/src/java/org/apache/cassandra/service/reads/range/RangeCommandIterator.java b/src/java/org/apache/cassandra/service/reads/range/RangeCommandIterator.java
index 62725a1d6d..bf9885e224 100644
--- a/src/java/org/apache/cassandra/service/reads/range/RangeCommandIterator.java
+++ b/src/java/org/apache/cassandra/service/reads/range/RangeCommandIterator.java
@@ -61,6 +61,7 @@ import org.apache.cassandra.service.reads.DataResolver;
import org.apache.cassandra.service.reads.ReadCallback;
import org.apache.cassandra.service.reads.ReadCoordinator;
import org.apache.cassandra.service.reads.repair.ReadRepair;
+import org.apache.cassandra.service.reads.tracked.TrackedRead;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.transport.Dispatcher;
@@ -208,7 +209,8 @@ public class RangeCommandIterator extends AbstractIterator<RowIterator> implemen
// If enabled, request repaired data tracking info from full replicas, but
// only if there are multiple full replicas to compare results from.
boolean trackRepairedStatus = DatabaseDescriptor.getRepairedDataTrackingForRangeReadsEnabled()
- && replicaPlan.contacts().filter(Replica::isFull).size() > 1;
+ && replicaPlan.contacts().filter(Replica::isFull).size() > 1
+ && !command.metadata().replicationType().isTracked();
ReplicaPlan.SharedForRangeRead sharedReplicaPlan = ReplicaPlan.shared(replicaPlan);
ReadRepair<EndpointsForRange, ReplicaPlan.ForRangeRead> readRepair =
@@ -329,7 +331,39 @@ public class RangeCommandIterator extends AbstractIterator<RowIterator> implemen
command.metadata().enforceStrictLiveness());
}
- PartitionIterator sendNextRequests()
+ private PartitionIterator sendNextRequestsTracked()
+ {
+ List<PartitionIterator> concurrentQueries = new ArrayList<>(concurrencyFactor);
+
+ try
+ {
+ for (int i = 0; i < concurrencyFactor && replicaPlans.hasNext(); )
+ {
+ ReplicaPlan.ForRangeRead replicaPlan = replicaPlans.next();
+ PartitionRangeReadCommand rangeCommand = command.forSubRange(replicaPlan.range(), i == 0);
+
+ TrackedRead.Range read = TrackedRead.Range.create(rangeCommand, replicaPlan);
+ read.start(requestTime);
+ concurrentQueries.add(read.iterator());
+
+ // due to RangeMerger, coordinator may fetch more ranges than required by concurrency factor.
+ rangesQueried += replicaPlan.vnodeCount();
+ i += replicaPlan.vnodeCount();
+ }
+ batchesRequested++;
+ }
+ catch (Throwable t)
+ {
+ for (PartitionIterator response : concurrentQueries)
+ response.close();
+ throw t;
+ }
+ Tracing.trace("Submitted {} concurrent range requests", concurrentQueries.size());
+
+ return PartitionIterators.concat(concurrentQueries);
+ }
+
+ PartitionIterator sendNextRequestsUntracked()
{
List<PartitionIterator> concurrentQueries = new ArrayList<>(concurrencyFactor);
List<ReadRepair<?, ?>> readRepairs = new ArrayList<>(concurrencyFactor);
@@ -366,13 +400,29 @@ public class RangeCommandIterator extends AbstractIterator<RowIterator> implemen
response.close();
throw t;
}
-
Tracing.trace("Submitted {} concurrent range requests", concurrentQueries.size());
+
+ return StorageProxy.concatAndBlockOnRepair(concurrentQueries, readRepairs);
+ }
+
+ PartitionIterator sendNextRequests()
+ {
+ PartitionIterator result;
+ if (command.metadata().replicationType().isTracked())
+ {
+ result = sendNextRequestsTracked();
+
+ }
+ else
+ {
+ result = sendNextRequestsUntracked();
+ }
+
// We want to count the results for the sake of updating the concurrency factor (see updateConcurrencyFactor)
// but we don't want to enforce any particular limit at this point (this could break code than rely on
// postReconciliationProcessing), hence the DataLimits.NONE.
counter = DataLimits.NONE.newCounter(command.nowInSec(), true, command.selectsFullPartition(), enforceStrictLiveness);
- return counter.applyTo(StorageProxy.concatAndBlockOnRepair(concurrentQueries, readRepairs));
+ return counter.applyTo(result);
}
// Wrap the iterator to retry if request routing is incorrect
diff --git a/src/java/org/apache/cassandra/service/reads/range/SingleRangeResponse.java b/src/java/org/apache/cassandra/service/reads/range/SingleRangeResponse.java
index 0274058030..a12b2fc984 100644
--- a/src/java/org/apache/cassandra/service/reads/range/SingleRangeResponse.java
+++ b/src/java/org/apache/cassandra/service/reads/range/SingleRangeResponse.java
@@ -33,7 +33,7 @@ class SingleRangeResponse extends AbstractIterator<RowIterator> implements Parti
{
private final DataResolver<EndpointsForRange, ReplicaPlan.ForRangeRead> resolver;
private final ReadCallback<EndpointsForRange, ReplicaPlan.ForRangeRead> handler;
- private final ReadRepair<EndpointsForRange, ReplicaPlan.ForRangeRead> readRepair;
+ protected final ReadRepair<EndpointsForRange, ReplicaPlan.ForRangeRead> readRepair;
private PartitionIterator result;
diff --git a/src/java/org/apache/cassandra/service/reads/repair/BlockingReadRepairs.java b/src/java/org/apache/cassandra/service/reads/repair/BlockingReadRepairs.java
index 3986a42bf4..2dc64d101c 100644
--- a/src/java/org/apache/cassandra/service/reads/repair/BlockingReadRepairs.java
+++ b/src/java/org/apache/cassandra/service/reads/repair/BlockingReadRepairs.java
@@ -31,6 +31,7 @@ import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.exceptions.ReadTimeoutException;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.tracing.Tracing;
@@ -53,7 +54,7 @@ public class BlockingReadRepairs
return null;
DecoratedKey key = update.partitionKey();
- Mutation mutation = new Mutation(update, potentialTxnConflicts);
+ Mutation mutation = new Mutation(MutationId.fixme(), update, potentialTxnConflicts);
int messagingVersion = MessagingService.instance().versions.get(destination);
try
diff --git a/src/java/org/apache/cassandra/service/reads/tracked/AbstractPartialTrackedRead.java b/src/java/org/apache/cassandra/service/reads/tracked/AbstractPartialTrackedRead.java
new file mode 100644
index 0000000000..0cbb3be38d
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/reads/tracked/AbstractPartialTrackedRead.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.service.reads.tracked;
+
+import java.util.List;
+
+import com.google.common.base.Preconditions;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.ReadExecutionController;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
+import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators;
+import org.apache.cassandra.db.transform.RTBoundValidator;
+import org.apache.cassandra.index.Index;
+
+import static org.apache.cassandra.db.partitions.UnfilteredPartitionIterators.MergeListener.NOOP;
+
+public abstract class AbstractPartialTrackedRead implements PartialTrackedRead
+{
+ private static final Logger logger = LoggerFactory.getLogger(AbstractPartialTrackedRead.class);
+
+ private enum State
+ {
+ INITIALIZED,
+ PREPARED,
+ READING,
+ FINISHED
+ }
+
+ final ReadExecutionController executionController;
+ final Index.Searcher searcher;
+ final ColumnFamilyStore cfs;
+ final long startTimeNanos;
+ volatile State state = State.INITIALIZED;
+
+ public AbstractPartialTrackedRead(ReadExecutionController executionController, Index.Searcher searcher, ColumnFamilyStore cfs, long startTimeNanos)
+ {
+ this.executionController = executionController;
+ this.searcher = searcher;
+ this.cfs = cfs;
+ this.startTimeNanos = startTimeNanos;
+ }
+
+ @Override
+ public ReadExecutionController executionController()
+ {
+ return executionController;
+ }
+
+ @Override
+ public Index.Searcher searcher()
+ {
+ return searcher;
+ }
+
+ @Override
+ public ColumnFamilyStore cfs()
+ {
+ return cfs;
+ }
+
+ @Override
+ public long startTimeNanos()
+ {
+ return startTimeNanos;
+ }
+
+ abstract void freezeInitialData();
+
+ abstract UnfilteredPartitionIterator initialData();
+
+ abstract UnfilteredPartitionIterator augmentedData();
+
+ abstract void augmentResponse(PartitionUpdate update);
+
+ /**
+ * Implementors need to call this before returning this from createInProgressRead
+ */
+ synchronized void prepare()
+ {
+ logger.trace("Preparing read {}", this);
+ Preconditions.checkState(state == State.INITIALIZED);
+ freezeInitialData();
+ state = State.PREPARED;
+ }
+
+ @Override
+ public void augment(Mutation mutation)
+ {
+ Preconditions.checkState(state == State.PREPARED);
+ PartitionUpdate update = mutation.getPartitionUpdate(command().metadata());
+ if (update != null)
+ augmentResponse(update);
+ }
+
+ private UnfilteredPartitionIterator complete(UnfilteredPartitionIterator iterator)
+ {
+ return command().completeTrackedRead(iterator, this);
+ }
+
+ abstract CompletedRead createResult(UnfilteredPartitionIterator iterator);
+
+ @Override
+ public synchronized CompletedRead complete()
+ {
+ Preconditions.checkState(state == State.PREPARED);
+ state = State.READING;
+
+ UnfilteredPartitionIterator initial = initialData();
+ UnfilteredPartitionIterator augmented = augmentedData();
+
+ UnfilteredPartitionIterator result = augmented != null ?
+ UnfilteredPartitionIterators.merge(List.of(initial, augmented), NOOP) :
+ initial;
+
+ result = complete(result);
+ // validate that the sequence of RT markers is correct: open is followed by close, deletion times for both
+ // ends equal, and there are no dangling RT bound in any partition.
+ result = RTBoundValidator.validate(result, RTBoundValidator.Stage.PROCESSED, true);
+ return createResult(complete(result));
+ }
+
+ @Override
+ public synchronized void close()
+ {
+ if (state == State.FINISHED)
+ return;
+
+ logger.trace("Closing read {}", this);
+ executionController.close();
+ state = State.FINISHED;
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/reads/tracked/PartialTrackedRangeRead.java b/src/java/org/apache/cassandra/service/reads/tracked/PartialTrackedRangeRead.java
new file mode 100644
index 0000000000..4f50442950
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/reads/tracked/PartialTrackedRangeRead.java
@@ -0,0 +1,347 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.reads.tracked;
+
+import java.util.Iterator;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import com.google.common.base.Preconditions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.ConsistencyLevel;
+import org.apache.cassandra.db.DataRange;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.PartitionPosition;
+import org.apache.cassandra.db.PartitionRangeReadCommand;
+import org.apache.cassandra.db.ReadCommand;
+import org.apache.cassandra.db.ReadExecutionController;
+import org.apache.cassandra.db.filter.DataLimits;
+import org.apache.cassandra.db.partitions.AbstractBTreePartition;
+import org.apache.cassandra.db.partitions.AbstractUnfilteredPartitionIterator;
+import org.apache.cassandra.db.partitions.PartitionIterator;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.partitions.SimpleBTreePartition;
+import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
+import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
+import org.apache.cassandra.db.transform.EmptyPartitionsDiscarder;
+import org.apache.cassandra.db.transform.Transformation;
+import org.apache.cassandra.dht.AbstractBounds;
+import org.apache.cassandra.dht.ExcludingBounds;
+import org.apache.cassandra.dht.Range;
+import org.apache.cassandra.index.Index;
+import org.apache.cassandra.index.transactions.UpdateTransaction;
+import org.apache.cassandra.locator.ReplicaPlan;
+import org.apache.cassandra.locator.ReplicaPlans;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.tracing.Tracing;
+import org.apache.cassandra.utils.FBUtilities;
+
+public class PartialTrackedRangeRead extends AbstractPartialTrackedRead
+{
+ private static final Logger logger = LoggerFactory.getLogger(PartialTrackedRangeRead.class);
+
+ private final PartitionRangeReadCommand command;
+ private final SortedMap<DecoratedKey, SimpleBTreePartition> data = new TreeMap<>();
+ private final UnfilteredPartitionIterator initialData;
+ private final boolean enforceStrictLiveness;
+
+ // short read support
+ private DecoratedKey lastPartitionKey; // key of the last observed partition
+ private boolean partitionsFetched; // whether we've seen any new partitions since iteration start or last moreContents() call
+ private boolean initialIteratorExhausted;
+ private boolean wasAugmented;
+ AbstractBounds<PartitionPosition> followUpBounds;
+
+ public PartialTrackedRangeRead(ReadExecutionController executionController, Index.Searcher searcher, ColumnFamilyStore cfs, long startTimeNanos, PartitionRangeReadCommand command, UnfilteredPartitionIterator initialData)
+ {
+ super(executionController, searcher, cfs, startTimeNanos);
+ this.command = command;
+ this.initialData = initialData;
+ this.enforceStrictLiveness = command.metadata().enforceStrictLiveness();
+ }
+
+ public static PartialTrackedRangeRead create(ReadExecutionController executionController, Index.Searcher searcher, ColumnFamilyStore cfs, long startTimeNanos, PartitionRangeReadCommand command, UnfilteredPartitionIterator initialData)
+ {
+ PartialTrackedRangeRead read = new PartialTrackedRangeRead(executionController, searcher, cfs, startTimeNanos, command, initialData);
+ try
+ {
+ read.prepare();
+ return read;
+ }
+ catch (Throwable e)
+ {
+ read.close();
+ throw e;
+ }
+ }
+
+ @Override
+ public ReadCommand command()
+ {
+ return command;
+ }
+
+ UnfilteredRowIterator queryPartition(AbstractBTreePartition partition)
+ {
+ return partition.unfilteredIterator(command.columnFilter(),
+ command.requestedSlices(),
+ command.clusteringIndexFilter(partition.partitionKey()).isReversed());
+ }
+
+ private static void consume(UnfilteredPartitionIterator iterator)
+ {
+ while (iterator.hasNext())
+ {
+ try (UnfilteredRowIterator partition = iterator.next())
+ {
+ while (partition.hasNext())
+ partition.next();
+ }
+ }
+ }
+
+ @Override
+ void freezeInitialData()
+ {
+ // memtable contents are frozen at read completion time, when the iterator is evaluated, not at the beginning
+ // of the read, when references to memtables and sstables are collected. Because of this, replica coordinated
+ // reads can cause read monotonicity to be broken by returning data that hasn't been replicated to at least
+ // CL other nodes via reconciliation. To prevent this, the contents of the initial iterator are materialized
+ // onto heap at partition granularity until the limits of the read are reached.
+
+ UnfilteredPartitionIterator materializer = new AbstractUnfilteredPartitionIterator()
+ {
+ @Override
+ public TableMetadata metadata()
+ {
+ return initialData.metadata();
+ }
+
+ @Override
+ public boolean hasNext()
+ {
+ return initialData.hasNext();
+ }
+
+ @Override
+ public UnfilteredRowIterator next()
+ {
+ try (UnfilteredRowIterator rowIterator = initialData.next())
+ {
+ SimpleBTreePartition partition = augmentResponseInternal(PartitionUpdate.fromIterator(rowIterator, command.columnFilter()));
+ lastPartitionKey = partition.partitionKey();
+ partitionsFetched = true;
+ return queryPartition(partition);
+ }
+ }
+
+ @Override
+ public void close()
+ {
+ super.close();
+ initialData.close();
+ }
+ };
+
+ // unmerged per-source counter
+ final DataLimits.Counter singleResultCounter = command.limits().newCounter(command.nowInSec(),
+ false,
+ command.selectsFullPartition(),
+ enforceStrictLiveness);
+ try (UnfilteredPartitionIterator iterator = singleResultCounter.applyTo(materializer))
+ {
+ consume(iterator);
+ }
+ initialIteratorExhausted = command.limits().isExhausted(singleResultCounter);
+ if (partitionsFetched)
+ {
+ AbstractBounds<PartitionPosition> bounds = command.dataRange().keyRange();
+ followUpBounds = bounds.inclusiveRight()
+ ? new Range<>(lastPartitionKey, bounds.right)
+ : new ExcludingBounds<>(lastPartitionKey, bounds.right);
+ Preconditions.checkState(!followUpBounds.contains(lastPartitionKey));
+ }
+ wasAugmented = false;
+ }
+
+ @Override
+ UnfilteredPartitionIterator initialData()
+ {
+ Iterator<SimpleBTreePartition> iterator = data.values().iterator();
+ return new AbstractUnfilteredPartitionIterator()
+ {
+ @Override
+ public TableMetadata metadata()
+ {
+ return command.metadata();
+ }
+
+ @Override
+ public boolean hasNext()
+ {
+ return iterator.hasNext();
+ }
+
+ @Override
+ public UnfilteredRowIterator next()
+ {
+ return queryPartition(iterator.next());
+ }
+ };
+ }
+
+ @Override
+ UnfilteredPartitionIterator augmentedData()
+ {
+ return null;
+ }
+
+ private SimpleBTreePartition augmentResponseInternal(PartitionUpdate update)
+ {
+ SimpleBTreePartition partition = data.computeIfAbsent(update.partitionKey(), key -> new SimpleBTreePartition(key, update.metadata(), UpdateTransaction.NO_OP));
+ partition.update(update);
+ return partition;
+ }
+
+ @Override
+ void augmentResponse(PartitionUpdate update)
+ {
+ // if the input iterator reached the row limit, then we can't apply any augmenting mutations that are past
+ // the last materialized key. Since we wouldn't have materialized the local data for that key, applying an
+ // update would cause us to return incomplete data for it.
+ if (initialIteratorExhausted || !followUpBounds.contains(update.partitionKey()))
+ augmentResponseInternal(update);
+ wasAugmented = true;
+ }
+
+ private class ExtendingCompletedRead implements CompletedRead
+ {
+
+ final UnfilteredPartitionIterator iterator;
+ // merged end-result counter
+ final DataLimits.Counter mergedResultCounter = command.limits().newCounter(command.nowInSec(),
+ true,
+ command.selectsFullPartition(),
+ enforceStrictLiveness);
+
+ public ExtendingCompletedRead(UnfilteredPartitionIterator iterator)
+ {
+ this.iterator = iterator;
+ }
+
+ @Override
+ public PartitionIterator iterator()
+ {
+ PartitionIterator filtered = UnfilteredPartitionIterators.filter(iterator, command.nowInSec());
+ PartitionIterator counted = Transformation.apply(filtered, mergedResultCounter);
+ return Transformation.apply(counted, new EmptyPartitionsDiscarder());
+ }
+
+ @Override
+ public TrackedRead<?, ?> followupRead(ConsistencyLevel consistencyLevel, long expiresAtNanos)
+ {
+ // never try to request additional partitions from replicas if our reconciled partitions are already filled to the limit
+ if (mergedResultCounter.isDone())
+ return null;
+
+ // we do not apply short read protection when we have no limits at all
+ if (command.limits().isUnlimited())
+ return null;
+
+ /*
+ * If this is a single partition read command or an (indexed) partition range read command with
+ * a partition key specified, then we can't and shouldn't try fetch more partitions.
+ */
+ if (command.isLimitedToOnePartition())
+ return null;
+
+ /*
+ * If the returned result doesn't have enough rows/partitions to satisfy even the original limit, don't ask for more.
+ *
+ * Can only take the short cut if there is no per partition limit set. Otherwise it's possible to hit false
+ * positives due to some rows being uncounted for in certain scenarios (see CASSANDRA-13911).
+ */
+ if (initialIteratorExhausted && command.limits().perPartitionCount() == DataLimits.NO_LIMIT)
+ return null;
+
+ /*
+ * Either we had an empty iterator as the initial response, or our moreContents() call got us an empty iterator.
+ * There is no point to ask the replica for more rows - it has no more in the requested range.
+ */
+ if (!partitionsFetched)
+ return null;
+ partitionsFetched = false;
+
+ /*
+ * We are going to fetch one partition at a time for thrift and potentially more for CQL.
+ * The row limit will either be set to the per partition limit - if the command has no total row limit set, or
+ * the total # of rows remaining - if it has some. If we don't grab enough rows in some of the partitions,
+ * then future ShortReadRowsProtection.moreContents() calls will fetch the missing ones.
+ */
+ int toQuery = command.limits().count() != DataLimits.NO_LIMIT
+ ? command.limits().count() - mergedResultCounter.rowsCounted()
+ : command.limits().perPartitionCount();
+
+ ColumnFamilyStore.metricsFor(command.metadata().id).shortReadProtectionRequests.mark();
+ Tracing.trace("Requesting {} extra rows from {} for short read protection", toQuery, FBUtilities.getBroadcastAddressAndPort());
+ logger.info("Requesting {} extra rows from {} for short read protection", toQuery, FBUtilities.getBroadcastAddressAndPort());
+
+ return makeFollowupRead(toQuery, consistencyLevel, expiresAtNanos);
+ }
+
+ private TrackedRead<?, ?> makeFollowupRead(int toQuery, ConsistencyLevel consistencyLevel, long expiresAtNanos)
+ {
+ DataLimits newLimits = command.limits().forShortReadRetry(toQuery);
+
+ DataRange newDataRange = command.dataRange().forSubRange(followUpBounds);
+
+ Keyspace keyspace = Keyspace.open(command.metadata().keyspace);
+ PartitionRangeReadCommand followUpCmd = command.withUpdatedLimitsAndDataRange(newLimits, newDataRange);
+ ReplicaPlan.ForRangeRead replicaPlan = ReplicaPlans.forRangeRead(keyspace,
+ followUpCmd.indexQueryPlan(),
+ consistencyLevel,
+ followUpCmd.dataRange().keyRange(),
+ 1);
+
+ TrackedRead.Range read = TrackedRead.Range.create(followUpCmd, replicaPlan);
+ logger.trace("Short read detected, starting followup read {}", read);
+ read.start(expiresAtNanos);
+ return read;
+ }
+
+ @Override
+ public void close()
+ {
+ iterator.close();
+ }
+ }
+
+ @Override
+ CompletedRead createResult(UnfilteredPartitionIterator iterator)
+ {
+ if (wasAugmented)
+ return new ExtendingCompletedRead(iterator);
+ return CompletedRead.simple(iterator, command().nowInSec());
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/reads/tracked/PartialTrackedRead.java b/src/java/org/apache/cassandra/service/reads/tracked/PartialTrackedRead.java
new file mode 100644
index 0000000000..daf222239c
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/reads/tracked/PartialTrackedRead.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.reads.tracked;
+
+import java.util.Collection;
+
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.ConsistencyLevel;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.ReadCommand;
+import org.apache.cassandra.db.ReadExecutionController;
+import org.apache.cassandra.db.partitions.PartitionIterator;
+import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
+import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators;
+import org.apache.cassandra.index.Index;
+
+public interface PartialTrackedRead
+{
+ interface CompletedRead extends AutoCloseable
+ {
+ PartitionIterator iterator();
+ TrackedRead<?, ?> followupRead(ConsistencyLevel consistencyLevel, long expiresAtNanos);
+
+ @Override
+ void close();
+
+ static CompletedRead simple(UnfilteredPartitionIterator partition, long nowInSec)
+ {
+ return new CompletedRead()
+ {
+ @Override
+ public PartitionIterator iterator()
+ {
+ return UnfilteredPartitionIterators.filter(partition, nowInSec);
+ }
+
+ @Override
+ public TrackedRead<?, ?> followupRead(ConsistencyLevel consistencyLevel, long expiresAtNanos)
+ {
+ return null;
+ }
+
+ @Override
+ public void close()
+ {
+ partition.close();
+ }
+ };
+ }
+ }
+
+ CompletedRead complete();
+
+ void augment(Mutation mutation);
+
+ default void augment(Collection<Mutation> mutations)
+ {
+ mutations.forEach(this::augment);
+ }
+
+ ReadExecutionController executionController();
+
+ Index.Searcher searcher();
+
+ ColumnFamilyStore cfs();
+
+ long startTimeNanos();
+
+ ReadCommand command();
+
+ void close();
+}
diff --git a/src/java/org/apache/cassandra/service/reads/tracked/PartialTrackedSinglePartitionRead.java b/src/java/org/apache/cassandra/service/reads/tracked/PartialTrackedSinglePartitionRead.java
new file mode 100644
index 0000000000..6aa64a2985
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/reads/tracked/PartialTrackedSinglePartitionRead.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.reads.tracked;
+
+import com.google.common.base.Preconditions;
+
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.ReadCommand;
+import org.apache.cassandra.db.ReadExecutionController;
+import org.apache.cassandra.db.SinglePartitionReadCommand;
+import org.apache.cassandra.db.Slices;
+import org.apache.cassandra.db.partitions.PartitionUpdate;
+import org.apache.cassandra.db.partitions.SimpleBTreePartition;
+import org.apache.cassandra.db.partitions.SingletonUnfilteredPartitionIterator;
+import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
+import org.apache.cassandra.db.rows.UnfilteredRowIterator;
+import org.apache.cassandra.index.Index;
+import org.apache.cassandra.index.transactions.UpdateTransaction;
+
+public class PartialTrackedSinglePartitionRead extends AbstractPartialTrackedRead
+{
+ private final SinglePartitionReadCommand command;
+ private final UnfilteredPartitionIterator initialData;
+ private SimpleBTreePartition augmentedData;
+
+ public PartialTrackedSinglePartitionRead(ReadExecutionController executionController, Index.Searcher searcher, ColumnFamilyStore cfs, long startTimeNanos, SinglePartitionReadCommand command, UnfilteredPartitionIterator initialData)
+ {
+ super(executionController, searcher, cfs, startTimeNanos);
+ this.command = command;
+ this.initialData = initialData;
+ }
+
+ public static PartialTrackedSinglePartitionRead create(ReadExecutionController executionController, Index.Searcher searcher, ColumnFamilyStore cfs, long startTimeNanos, SinglePartitionReadCommand command, UnfilteredPartitionIterator initialData)
+ {
+ PartialTrackedSinglePartitionRead read = new PartialTrackedSinglePartitionRead(executionController, searcher, cfs, startTimeNanos, command, initialData);
+ try
+ {
+ read.prepare();
+ return read;
+ }
+ catch (Throwable e)
+ {
+ read.close();
+ throw e;
+ }
+ }
+
+ @Override
+ void freezeInitialData()
+ {
+ // the iterators from queryStorage grabs sstable references and a
+ // snapshot of the memtable partition so we don't need to do anything here
+ }
+
+ @Override
+ public ReadCommand command()
+ {
+ return command;
+ }
+
+ @Override
+ UnfilteredPartitionIterator initialData()
+ {
+ return initialData;
+ }
+
+ @Override
+ UnfilteredPartitionIterator augmentedData()
+ {
+ if (augmentedData == null)
+ return null;
+ Slices slices = command.clusteringIndexFilter().getSlices(command.metadata());
+ UnfilteredRowIterator augmented = augmentedData.unfilteredIterator(command.columnFilter(), slices, command.clusteringIndexFilter().isReversed());
+ return new SingletonUnfilteredPartitionIterator(augmented);
+ }
+
+ @Override
+ void augmentResponse(PartitionUpdate update)
+ {
+ Preconditions.checkArgument(update.partitionKey().equals(command.partitionKey()));
+ if (augmentedData == null)
+ augmentedData = new SimpleBTreePartition(command.partitionKey(), command.metadata(), UpdateTransaction.NO_OP);
+
+ augmentedData.update(update);
+ }
+
+ @Override
+ CompletedRead createResult(UnfilteredPartitionIterator iterator)
+ {
+ return CompletedRead.simple(iterator, command().nowInSec());
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/reads/tracked/ReadReconcileNotify.java b/src/java/org/apache/cassandra/service/reads/tracked/ReadReconcileNotify.java
new file mode 100644
index 0000000000..3f0e3ca3a5
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/reads/tracked/ReadReconcileNotify.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.reads.tracked;
+
+import java.io.IOException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.net.IVerbHandler;
+import org.apache.cassandra.net.Message;
+import org.apache.cassandra.replication.MutationTrackingService;
+
+/**
+ * Notifies the read coordinator that this node has received mutations
+ * from the sending node
+ */
+public class ReadReconcileNotify
+{
+ private static final Logger logger = LoggerFactory.getLogger(ReadReconcileNotify.class);
+
+ public final TrackedRead.Id readId;
+ public final int syncId;
+
+ public ReadReconcileNotify(TrackedRead.Id readId, int syncId)
+ {
+ this.readId = readId;
+ this.syncId = syncId;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "ReadReconcileNotify{" +
+ "reconciliationId=" + readId +
+ ", syncId=" + syncId +
+ '}';
+ }
+
+ public static final IVerbHandler<ReadReconcileNotify> verbHandler = new IVerbHandler<ReadReconcileNotify>()
+ {
+ @Override
+ public void doVerb(Message<ReadReconcileNotify> message) throws IOException
+ {
+ ReadReconcileNotify notify = message.payload;
+ logger.trace("Received read reconcile notify from {}: {}", message.from(), notify);
+ MutationTrackingService.instance.localReads().acknowledgeSync(notify.readId, notify.syncId);
+ }
+ };
+
+ public static final IVersionedSerializer<ReadReconcileNotify> serializer = new IVersionedSerializer<ReadReconcileNotify>()
+ {
+ @Override
+ public void serialize(ReadReconcileNotify notify, DataOutputPlus out, int version) throws IOException
+ {
+ TrackedRead.Id.serializer.serialize(notify.readId, out, version);
+ out.writeInt(notify.syncId);
+ }
+
+ @Override
+ public ReadReconcileNotify deserialize(DataInputPlus in, int version) throws IOException
+ {
+ TrackedRead.Id readId = TrackedRead.Id.serializer.deserialize(in, version);
+ int syncId = in.readInt();
+ return new ReadReconcileNotify(readId, syncId);
+ }
+
+ @Override
+ public long serializedSize(ReadReconcileNotify notify, int version)
+ {
+ return TrackedRead.Id.serializer.serializedSize(notify.readId, version)
+ + TypeSizes.sizeof(notify.syncId);
+ }
+ };
+}
diff --git a/src/java/org/apache/cassandra/service/reads/tracked/ReadReconcileReceive.java b/src/java/org/apache/cassandra/service/reads/tracked/ReadReconcileReceive.java
new file mode 100644
index 0000000000..f39f347e57
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/reads/tracked/ReadReconcileReceive.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.reads.tracked;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.locator.InetAddressAndPort;
+import org.apache.cassandra.net.IVerbHandler;
+import org.apache.cassandra.net.Message;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.net.Verb;
+import org.apache.cassandra.replication.MutationTrackingService;
+import org.apache.cassandra.utils.CollectionSerializer;
+
+import static org.apache.cassandra.locator.InetAddressAndPort.Serializer.inetAddressAndPortSerializer;
+
+public class ReadReconcileReceive
+{
+ private static final Logger logger = LoggerFactory.getLogger(ReadReconcileReceive.class);
+
+ public final TrackedRead.Id readId;
+ public final int syncId;
+ public final InetAddressAndPort coordinator;
+ public final List<Mutation> mutations;
+
+ public ReadReconcileReceive(TrackedRead.Id readId, int syncId, InetAddressAndPort coordinator, List<Mutation> mutations)
+ {
+ this.readId = readId;
+ this.syncId = syncId;
+ this.coordinator = coordinator;
+ this.mutations = mutations;
+ }
+
+ private static String mutationString(List<Mutation> mutations)
+ {
+ StringBuilder builder = new StringBuilder('[');
+ boolean isFirst = true;
+ for (Mutation mutation : mutations)
+ {
+ if (!isFirst)
+ {
+ builder.append(", ");
+ }
+ isFirst = false;
+ builder.append(mutation.id());
+ }
+ builder.append(']');
+ return builder.toString();
+ }
+
+ @Override
+ public String toString()
+ {
+ return "ReadReconcileReceive{" +
+ "reconciliationId=" + readId +
+ ", syncId=" + syncId +
+ ", coordinator=" + coordinator +
+ ", mutations=" + mutationString(mutations) +
+ '}';
+ }
+
+ public static final IVerbHandler<ReadReconcileReceive> verbHandler = new IVerbHandler<ReadReconcileReceive>()
+ {
+ @Override
+ public void doVerb(Message<ReadReconcileReceive> message) throws IOException
+ {
+ // TODO: check epoch and tokens?
+ ReadReconcileReceive receive = message.payload;
+ logger.trace("Received read reconciliation from {}: {}", message.from(), receive);
+ receive.mutations.forEach(Mutation::apply);
+
+ if (!MutationTrackingService.instance.localReads().receiveMutations(receive.readId, receive.syncId, receive.mutations))
+ {
+ // if this isn't a locally coordinated read, notify the coordinator
+ ReadReconcileNotify notify = new ReadReconcileNotify(receive.readId, receive.syncId);
+ MessagingService.instance().send(Message.out(Verb.READ_RECONCILE_NOTIFY, notify), receive.coordinator);
+ }
+ }
+ };
+
+ public static final IVersionedSerializer<ReadReconcileReceive> serializer = new IVersionedSerializer<ReadReconcileReceive>()
+ {
+ @Override
+ public void serialize(ReadReconcileReceive rcv, DataOutputPlus out, int version) throws IOException
+ {
+ TrackedRead.Id.serializer.serialize(rcv.readId, out, version);
+ out.writeInt(rcv.syncId);
+ inetAddressAndPortSerializer.serialize(rcv.coordinator, out, version);
+ CollectionSerializer.serializeCollection(Mutation.serializer, rcv.mutations, out, version);
+
+ }
+
+ @Override
+ public ReadReconcileReceive deserialize(DataInputPlus in, int version) throws IOException
+ {
+ TrackedRead.Id readId = TrackedRead.Id.serializer.deserialize(in, version);
+ int syncId = in.readInt();
+ InetAddressAndPort coordinator = inetAddressAndPortSerializer.deserialize(in, version);
+ List<Mutation> mutations = CollectionSerializer.deserializeCollection(Mutation.serializer, ArrayList::new, in, version);
+ return new ReadReconcileReceive(readId, syncId, coordinator, mutations);
+ }
+
+ @Override
+ public long serializedSize(ReadReconcileReceive t, int version)
+ {
+ return TrackedRead.Id.serializer.serializedSize(t.readId, version)
+ + TypeSizes.sizeof(t.syncId)
+ + inetAddressAndPortSerializer.serializedSize(t.coordinator, version)
+ + CollectionSerializer.serializedSizeCollection(Mutation.serializer, t.mutations, version);
+ }
+ };
+}
diff --git a/src/java/org/apache/cassandra/service/reads/tracked/ReadReconcileSend.java b/src/java/org/apache/cassandra/service/reads/tracked/ReadReconcileSend.java
new file mode 100644
index 0000000000..e51ea282b0
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/reads/tracked/ReadReconcileSend.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.service.reads.tracked;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.locator.InetAddressAndPort;
+import org.apache.cassandra.net.IVerbHandler;
+import org.apache.cassandra.net.Message;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.net.Verb;
+import org.apache.cassandra.replication.Log2OffsetsMap;
+import org.apache.cassandra.replication.MutationJournal;
+import org.apache.cassandra.utils.CollectionSerializer;
+
+/**
+ * Instructs a node to send mutations to other node
+ */
+public class ReadReconcileSend
+{
+ private static final Logger logger = LoggerFactory.getLogger(ReadReconcileSend.class);
+
+ public static class PeerSync
+ {
+ final int syncId;
+ final InetAddressAndPort to;
+ final Log2OffsetsMap.Immutable plan;
+
+ public PeerSync(int syncId, InetAddressAndPort to, Log2OffsetsMap.Immutable plan)
+ {
+ this.syncId = syncId;
+ this.to = to;
+ this.plan = plan;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "PeerSync{" +
+ "syncId=" + syncId +
+ ", to=" + to +
+ ", plan=" + plan +
+ '}';
+ }
+
+ public static final IVersionedSerializer<PeerSync> serializer = new IVersionedSerializer<PeerSync>()
+ {
+ @Override
+ public void serialize(PeerSync sync, DataOutputPlus out, int version) throws IOException
+ {
+ out.writeInt(sync.syncId);
+ InetAddressAndPort.Serializer.inetAddressAndPortSerializer.serialize(sync.to, out, version);
+ Log2OffsetsMap.Immutable.serializer.serialize(sync.plan, out, version);
+ }
+
+ @Override
+ public PeerSync deserialize(DataInputPlus in, int version) throws IOException
+ {
+ return new PeerSync(in.readInt(),
+ InetAddressAndPort.Serializer.inetAddressAndPortSerializer.deserialize(in, version),
+ Log2OffsetsMap.Immutable.serializer.deserialize(in, version));
+ }
+
+ @Override
+ public long serializedSize(PeerSync sync, int version)
+ {
+ return TypeSizes.sizeof(sync.syncId)
+ + InetAddressAndPort.Serializer.inetAddressAndPortSerializer.serializedSize(sync.to, version)
+ + Log2OffsetsMap.Immutable.serializer.serializedSize(sync.plan, version);
+ }
+ };
+ }
+
+ public final TrackedRead.Id reconcileId;
+ public final ImmutableList<PeerSync> syncTasks;
+
+ public ReadReconcileSend(TrackedRead.Id reconcileId, List<PeerSync> syncTasks)
+ {
+ this.reconcileId = reconcileId;
+ this.syncTasks = ImmutableList.copyOf(syncTasks);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "ReadReconcileSend{" +
+ "reconcileId=" + reconcileId +
+ ", syncTasks=" + syncTasks +
+ '}';
+ }
+
+ public static final IVerbHandler<ReadReconcileSend> verbHandler = new IVerbHandler<>()
+ {
+ @Override
+ public void doVerb(Message<ReadReconcileSend> message)
+ {
+ logger.trace("Received ReadReconcileSend from {}: {}", message.from(), message.payload);
+ // TODO: check epoch and tokens?
+ ReadReconcileSend payload = message.payload;
+ for (PeerSync sync : message.payload.syncTasks)
+ {
+ // TODO (expected): do not deser just to serialize again, if same messaging versions (common case)
+ // TODO (expected): don't materialize mutation ids, look up from offset collections
+ int mutationCount = sync.plan.idCount();
+ List<Mutation> mutations = new ArrayList<>(mutationCount);
+ MutationJournal.instance.readAll(sync.plan, mutations);
+ Preconditions.checkArgument(mutationCount == mutations.size());
+
+ ReadReconcileReceive receive = new ReadReconcileReceive(payload.reconcileId, sync.syncId, message.from(), mutations);
+ logger.trace("Sending {} to replica {}", receive, sync.to);
+ MessagingService.instance().send(Message.out(Verb.READ_RECONCILE_RCV, receive), sync.to);
+ }
+ }
+ };
+
+ public static final IVersionedSerializer<ReadReconcileSend> serializer = new IVersionedSerializer<>()
+ {
+ @Override
+ public void serialize(ReadReconcileSend send, DataOutputPlus out, int version) throws IOException
+ {
+ TrackedRead.Id.serializer.serialize(send.reconcileId, out, version);
+ CollectionSerializer.serializeList(PeerSync.serializer, send.syncTasks, out, version);
+ }
+
+ @Override
+ public ReadReconcileSend deserialize(DataInputPlus in, int version) throws IOException
+ {
+ TrackedRead.Id readId = TrackedRead.Id.serializer.deserialize(in, version);
+ List<PeerSync> syncTasks = CollectionSerializer.deserializeCollection(PeerSync.serializer, ArrayList::new, in, version);
+ return new ReadReconcileSend(readId, syncTasks);
+ }
+
+ @Override
+ public long serializedSize(ReadReconcileSend send, int version)
+ {
+ return TrackedRead.Id.serializer.serializedSize(send.reconcileId, version) + CollectionSerializer.serializedSizeCollection(PeerSync.serializer, send.syncTasks, version);
+ }
+ };
+}
diff --git a/src/java/org/apache/cassandra/service/reads/tracked/TrackedDataResponse.java b/src/java/org/apache/cassandra/service/reads/tracked/TrackedDataResponse.java
new file mode 100644
index 0000000000..b55095bf02
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/reads/tracked/TrackedDataResponse.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.service.reads.tracked;
+
+import org.apache.cassandra.db.ReadCommand;
+import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.db.filter.ColumnFilter;
+import org.apache.cassandra.db.partitions.PartitionIterator;
+import org.apache.cassandra.db.partitions.PartitionIterators;
+import org.apache.cassandra.io.IVersionedSerializer;
+import org.apache.cassandra.io.util.DataInputBuffer;
+import org.apache.cassandra.io.util.DataInputPlus;
+import org.apache.cassandra.io.util.DataOutputBuffer;
+import org.apache.cassandra.io.util.DataOutputPlus;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+public class TrackedDataResponse
+{
+ private final int serializationVersion;
+ private final ByteBuffer data;
+
+ public TrackedDataResponse(int serializationVersion, ByteBuffer data)
+ {
+ this.serializationVersion = serializationVersion;
+ this.data = data;
+ }
+
+ public static TrackedDataResponse create(PartitionIterator iter, ColumnFilter selection)
+ {
+ try (DataOutputBuffer buffer = new DataOutputBuffer())
+ {
+ PartitionIterators.Serializer.serialize(iter, selection, buffer, MessagingService.current_version);
+ return new TrackedDataResponse(MessagingService.current_version, buffer.buffer(false));
+ }
+ catch (IOException e)
+ {
+ // We're serializing in memory so this shouldn't happen
+ throw new RuntimeException(e);
+ }
+ }
+
+ public PartitionIterator makeIterator(ReadCommand command)
+ {
+ try (DataInputBuffer in = new DataInputBuffer(data, true))
+ {
+ return PartitionIterators.Serializer.deserialize(command.metadata(), command.columnFilter(), in, serializationVersion);
+ }
+ catch (IOException e)
+ {
+ // We're deserializing in memory so this shouldn't happen
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static final IVersionedSerializer<TrackedDataResponse> serializer = new IVersionedSerializer<TrackedDataResponse>()
+ {
+ @Override
+ public void serialize(TrackedDataResponse response, DataOutputPlus out, int version) throws IOException
+ {
+ out.writeInt(response.serializationVersion);
+ ByteBufferUtil.writeWithVIntLength(response.data, out);
+ }
+
+ @Override
+ public TrackedDataResponse deserialize(DataInputPlus in, int version) throws IOException
+ {
+ int serializationVersion = in.readInt();
+ ByteBuffer data = ByteBufferUtil.readWithVIntLength(in);
+ return new TrackedDataResponse(serializationVersion, data);
+ }
+
+ @Override
+ public long serializedSize(TrackedDataResponse response, int version)
+ {
+ return TypeSizes.sizeof(response.serializationVersion)
+ + ByteBufferUtil.serializedSizeWithVIntLength(response.data);
+ }
+ };
+}
diff --git a/src/java/org/apache/cassandra/service/reads/tracked/TrackedLocalReadCoordinator.java b/src/java/org/apache/cassandra/service/reads/tracked/TrackedLocalReadCoordinator.java
new file mode 100644
index 0000000000..6732adca4b
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/reads/tracked/TrackedLocalReadCoordinator.java
@@ -0,0 +1,617 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.service.reads.tracked;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import org.apache.cassandra.concurrent.Stage;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.ConsistencyLevel;
+import org.apache.cassandra.db.Mutation;
+import org.apache.cassandra.db.ReadCommand;
+import org.apache.cassandra.db.ReadExecutionController;
+import org.apache.cassandra.db.filter.ColumnFilter;
+import org.apache.cassandra.db.partitions.PartitionIterator;
+import org.apache.cassandra.db.partitions.PartitionIterators;
+import org.apache.cassandra.locator.InetAddressAndPort;
+import org.apache.cassandra.locator.ReplicaPlan;
+import org.apache.cassandra.metrics.ReadRepairMetrics;
+import org.apache.cassandra.net.Message;
+import org.apache.cassandra.net.MessagingService;
+import org.apache.cassandra.net.Verb;
+import org.apache.cassandra.replication.Log2OffsetsMap;
+import org.apache.cassandra.replication.MutationJournal;
+import org.apache.cassandra.replication.MutationSummary;
+import org.apache.cassandra.replication.ReconciliationPlan;
+import org.apache.cassandra.replication.ShortMutationId;
+import org.apache.cassandra.utils.Clock;
+import org.apache.cassandra.utils.FBUtilities;
+import org.apache.cassandra.utils.concurrent.AbstractFuture;
+import org.apache.cassandra.utils.concurrent.Accumulator;
+import org.apache.cassandra.utils.concurrent.AsyncPromise;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+
+public class TrackedLocalReadCoordinator
+{
+ private static final Logger logger = LoggerFactory.getLogger(TrackedLocalReadCoordinator.class);
+
+ private final TrackedRead.Id readId;
+ private final AsyncPromise<TrackedDataResponse> promise;
+
+ private volatile State state;
+
+ public TrackedLocalReadCoordinator(TrackedRead.Id readId)
+ {
+ this.readId = readId;
+ this.promise = new AsyncPromise<>();
+ this.state = INITIALIZED;
+ }
+
+ public AbstractFuture<TrackedDataResponse> addCallback(BiConsumer<TrackedDataResponse, Throwable> callback)
+ {
+ return promise.addCallback(callback);
+ }
+
+ enum Status { INITIALIZED, AWAITING_READ, READING, RECONCILING, ABORTED, COMPLETED }
+
+ private static abstract class State
+ {
+ abstract Status status();
+
+ State startLocalRead(TrackedRead.Id readId, AsyncPromise<TrackedDataResponse> promise, ReadCommand command, ReplicaPlan.AbstractForRead<?, ?> replicaPlan, int[] summaryNodes, long expiresAtNanos)
+ {
+ // TODO: validate permitted state instead of just ignoring events?
+ return this;
+ }
+
+ State receiveInProgressRead(PartialTrackedRead read, MutationSummary summary)
+ {
+ throw new IllegalStateException("Received in-progress read with status " + status());
+ }
+
+ State receiveSummary(InetAddressAndPort from, MutationSummary summary)
+ {
+ logger.trace("Ignoring summary from {} with status {}", from, status());
+ return this;
+ }
+
+ State acknowledgeSync(int syncId)
+ {
+ logger.trace("Ignoring sync ack {} with status {}", syncId, status());
+ return this;
+ }
+
+ State receiveMutations(List<Mutation> mutations)
+ {
+ logger.trace("Ignoring {} mutations with status {}", mutations.size(), status());
+ return this;
+ }
+
+ State abort()
+ {
+ return ABORTED;
+ }
+
+ boolean isPurgeable(long nanoTime)
+ {
+ return false;
+ }
+
+ boolean isReading()
+ {
+ return is(Status.READING);
+ }
+
+ boolean isCompleted()
+ {
+ return is(Status.COMPLETED);
+ }
+
+ boolean is(Status status)
+ {
+ return status() == status;
+ }
+
+ @Override
+ public String toString()
+ {
+ return status().toString();
+ }
+ }
+
+ static final State INITIALIZED = new State()
+ {
+ @Override
+ Status status() { return Status.INITIALIZED; }
+
+ @Override
+ State startLocalRead(TrackedRead.Id readId, AsyncPromise<TrackedDataResponse> promise, ReadCommand command, ReplicaPlan.AbstractForRead<?, ?> replicaPlan, int[] summaryNodes, long expiresAtNanos)
+ {
+ return new Reading(readId, promise, command, replicaPlan, summaryNodes, expiresAtNanos);
+ }
+
+ @Override
+ State receiveSummary(InetAddressAndPort from, MutationSummary summary)
+ {
+ return new AwaitingRead(from, summary);
+ }
+ };
+
+ static final State ABORTED = new State()
+ {
+ @Override
+ Status status() { return Status.ABORTED; }
+
+ @Override
+ State receiveInProgressRead(PartialTrackedRead read, MutationSummary summary)
+ {
+ return this; // the read can't complete without data, but it could have been aborted in the meantime
+ }
+
+ @Override
+ boolean isPurgeable(long nanoTime) { return true; }
+ };
+
+ static final State COMPLETED = new State()
+ {
+ @Override
+ Status status() { return Status.COMPLETED; }
+
+ @Override
+ boolean isPurgeable(long nanoTime) { return true; }
+ };
+
+ private static class ReceivedSummary
+ {
+ final InetAddressAndPort from;
+ final MutationSummary summary;
+
+ ReceivedSummary(InetAddressAndPort from, MutationSummary summary)
+ {
+ this.from = from;
+ this.summary = summary;
+ }
+ }
+
+ // if we start receiving summaries before we receive the read command, they're
+ // collected here
+ private static class AwaitingRead extends State
+ {
+ private long lastUpdateNanos;
+ private final List<ReceivedSummary> summaries;
+
+ AwaitingRead(InetAddressAndPort summaryFrom, MutationSummary summary)
+ {
+ summaries = new ArrayList<>();
+ summaries.add(new ReceivedSummary(summaryFrom, summary));
+ }
+
+ @Override
+ Status status()
+ {
+ return Status.AWAITING_READ;
+ }
+
+ @Override
+ State startLocalRead(TrackedRead.Id readId, AsyncPromise<TrackedDataResponse> promise, ReadCommand command, ReplicaPlan.AbstractForRead<?, ?> replicaPlan, int[] summaryNodes, long expiresAtNanos)
+ {
+ return new Reading(readId, promise, command, replicaPlan, summaryNodes, summaries, expiresAtNanos);
+ }
+
+ @Override
+ State receiveSummary(InetAddressAndPort from, MutationSummary summary)
+ {
+ summaries.add(new ReceivedSummary(from, summary));
+ lastUpdateNanos = Clock.Global.nanoTime();
+ return this;
+ }
+
+ @Override
+ boolean isPurgeable(long nanoTime)
+ {
+ return nanoTime - lastUpdateNanos > DatabaseDescriptor.getReadRpcTimeout(TimeUnit.NANOSECONDS);
+ }
+ }
+
+ private static class Reading extends State
+ {
+ private final TrackedRead.Id readId;
+ private final AsyncPromise<TrackedDataResponse> promise;
+ private final ReadCommand command;
+ private volatile PartialTrackedRead read;
+ private final long expiresAtNanos;
+ private final ReplicaPlan.AbstractForRead<?, ?> replicaPlan;
+ private final Accumulator<ReceivedSummary> summaries;
+ private final int[] summaryNodes; // for speculating when we haven't received enough summaries
+
+ Reading(
+ TrackedRead.Id readId,
+ AsyncPromise<TrackedDataResponse> promise,
+ ReadCommand command,
+ ReplicaPlan.AbstractForRead<?, ?> replicaPlan,
+ int[] summaryNodes,
+ long expiresAtNanos)
+ {
+ this.readId = readId;
+ this.promise = promise;
+ this.expiresAtNanos = expiresAtNanos;
+ this.command = command;
+ this.replicaPlan = replicaPlan;
+ this.summaries = new Accumulator<>(replicaPlan.readCandidates().size());
+ this.summaryNodes = summaryNodes;
+ }
+
+ Reading(
+ TrackedRead.Id readId,
+ AsyncPromise<TrackedDataResponse> promise,
+ ReadCommand command,
+ ReplicaPlan.AbstractForRead<?, ?> replicaPlan,
+ int[] summaryNodes,
+ List<ReceivedSummary> summaries,
+ long expiresAtNanos)
+ {
+ this(readId, promise, command, replicaPlan, summaryNodes, expiresAtNanos);
+ for (ReceivedSummary summary : summaries) this.summaries.add(summary);
+ }
+
+ @Override
+ Status status()
+ {
+ return Status.READING;
+ }
+
+ @Override
+ boolean isPurgeable(long nanoTime)
+ {
+ return nanoTime - expiresAtNanos > 0;
+ }
+
+ private State maybeComplete()
+ {
+ if (read == null || summaries.size() < replicaPlan.readQuorum())
+ return this;
+
+ Map<InetAddressAndPort, MutationSummary> summaryMap = new HashMap<>();
+ summaries.snapshot().forEach(rc -> summaryMap.put(rc.from, rc.summary));
+
+ Map<InetAddressAndPort, ReconciliationPlan> reconciliations = ReconciliationPlan.calculateReconciliation(summaryMap);
+
+ if (reconciliations.isEmpty())
+ {
+ logger.trace("Read complete for {}", readId);
+ complete(promise, read, command.columnFilter(), replicaPlan.consistencyLevel(), expiresAtNanos);
+ return COMPLETED;
+ }
+ else
+ {
+ logger.trace("Beginning reconciliation for {}", readId);
+ Reconciling reconciling = new Reconciling(readId, promise, command, read, replicaPlan.consistencyLevel(), expiresAtNanos, reconciliations);
+ reconciling.start(); // TODO: don't do this until after the coordinator state is set to reconciling if converting to lock free
+ return reconciling;
+ }
+ }
+
+ @Override
+ State receiveInProgressRead(PartialTrackedRead read, MutationSummary summary)
+ {
+ if (this.read != null)
+ return this;
+
+ logger.trace("In progress read received for {}", readId);
+ this.read = read;
+ summaries.add(new ReceivedSummary(FBUtilities.getBroadcastAddressAndPort(), summary));
+
+ return maybeComplete();
+ }
+
+ @Override
+ State receiveSummary(InetAddressAndPort from, MutationSummary summary)
+ {
+ logger.trace("Summary received from {} for {}", from, readId);
+ summaries.add(new ReceivedSummary(from, summary));
+ return maybeComplete();
+ }
+
+ @Override
+ State abort()
+ {
+ logger.trace("Aborting read {}", readId);
+ if (read != null)
+ read.close();
+ return ABORTED;
+ }
+ }
+
+ private static class PendingSync
+ {
+ final int syncId;
+ final InetAddressAndPort from;
+ final InetAddressAndPort to;
+ final Log2OffsetsMap.Immutable plan;
+
+ PendingSync(int syncId, InetAddressAndPort from, InetAddressAndPort to, Log2OffsetsMap.Immutable plan)
+ {
+ this.syncId = syncId;
+ this.from = from;
+ this.to = to;
+ this.plan = plan;
+ }
+
+ ReadReconcileSend.PeerSync toPeerSync()
+ {
+ return new ReadReconcileSend.PeerSync(syncId, to, plan);
+ }
+ }
+
+ private static class Reconciling extends State
+ {
+ private final TrackedRead.Id readId;
+ private final AsyncPromise<TrackedDataResponse> promise;
+ private final ReadCommand command;
+ private final PartialTrackedRead read;
+ private final ConsistencyLevel consistencyLevel;
+ private final long expiresAtNanos;
+
+ final Map<InetAddressAndPort, ReconciliationPlan> plans;
+ final Log2OffsetsMap.Mutable outstandingMutations = new Log2OffsetsMap.Mutable();
+ final Map<Integer, PendingSync> pendingSync = new ConcurrentHashMap<>();
+ final int blockFor;
+
+ Reconciling(
+ TrackedRead.Id readId,
+ AsyncPromise<TrackedDataResponse> promise,
+ ReadCommand command,
+ PartialTrackedRead read,
+ ConsistencyLevel consistencyLevel,
+ long expiresAtNanos,
+ Map<InetAddressAndPort, ReconciliationPlan> plans)
+ {
+ this.readId = readId;
+ this.promise = promise;
+ this.command = command;
+ this.read = Preconditions.checkNotNull(read);
+ this.consistencyLevel = consistencyLevel;
+ this.expiresAtNanos = expiresAtNanos;
+ this.plans = plans;
+
+ int syncs = 0;
+ int nextSyncId = 0;
+ for (Map.Entry<InetAddressAndPort, ReconciliationPlan> entry : plans.entrySet())
+ {
+ InetAddressAndPort from = entry.getKey();
+ ReconciliationPlan plan = entry.getValue();
+ for (InetAddressAndPort to : plan.nodes())
+ {
+ int syncId = nextSyncId++;
+ PendingSync sync = new PendingSync(syncId, from, to, plan.peerReconciliation(to));
+ pendingSync.put(syncId, sync);
+ syncs++;
+ if (to.equals(FBUtilities.getBroadcastAddressAndPort()))
+ {
+ outstandingMutations.addAll(plan.offsetsFor(to));
+ }
+ }
+ }
+
+ if (logger.isTraceEnabled())
+ logger.trace("Reconciling {} syncs, {} mutations for {}", syncs, outstandingMutations.idCount(), readId);
+
+ this.blockFor = syncs;
+ }
+
+ @Override
+ Status status()
+ {
+ return Status.RECONCILING;
+ }
+
+ @Override
+ boolean isPurgeable(long nanoTime)
+ {
+ return nanoTime - expiresAtNanos > 0;
+ }
+
+ void start()
+ {
+ ReadRepairMetrics.trackedReconcile.mark();
+ ColumnFamilyStore.metricsFor(command.metadata().id).readRepairRequests.mark();
+
+ Map<InetAddressAndPort, List<ReadReconcileSend.PeerSync>> peerSync = new HashMap<>();
+ pendingSync.values().forEach(pending ->
+ peerSync.computeIfAbsent(pending.from, node -> new ArrayList<>()).add(pending.toPeerSync()));
+
+ for (Map.Entry<InetAddressAndPort, List<ReadReconcileSend.PeerSync>> entry : peerSync.entrySet())
+ {
+ Message<ReadReconcileSend> message = Message.out(Verb.READ_RECONCILE_SEND, new ReadReconcileSend(readId, entry.getValue()));
+ logger.trace("Sending read reconciliation for {} {} to {}", readId, message.payload, entry.getKey());
+ MessagingService.instance().send(message, entry.getKey());
+ }
+ }
+
+ private State maybeComplete()
+ {
+ if (!pendingSync.isEmpty() || !outstandingMutations.isEmpty())
+ return this;
+
+ logger.trace("Reconciliation completed for read {}", readId);
+ complete(promise, read, command.columnFilter(), consistencyLevel, expiresAtNanos);
+ return COMPLETED;
+ }
+
+ @Override
+ State acknowledgeSync(int syncId)
+ {
+ logger.trace("Reconciliation sync {} received for {}", syncId, readId);
+ pendingSync.remove(syncId);
+ return maybeComplete();
+ }
+
+ @Override
+ State receiveMutations(List<Mutation> mutations)
+ {
+ Log2OffsetsMap.Mutable received = new Log2OffsetsMap.Mutable();
+ mutations.forEach(mutation -> {
+ logger.trace("Received mutation {} for read {}", mutation.id(), readId);
+ received.add(mutation.id());
+ });
+ outstandingMutations.removeAll(received);
+
+ if (logger.isTraceEnabled())
+ logger.trace("Received {} mutations, {} mutations outstanding for {}", mutations.size(), outstandingMutations.idCount(), readId);
+ read.augment(mutations);
+ return maybeComplete();
+ }
+
+ @Override
+ State abort()
+ {
+ read.close();
+ return ABORTED;
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return "TrackedLocalReadCoordinator{" + readId + ':' + state.status() + '}';
+ }
+
+ @VisibleForTesting
+ public static void processDelta(PartialTrackedRead read, MutationSummary initialSummary, MutationSummary secondarySummary)
+ {
+ // Compute any mutations that we could've missed during initial read execution.
+ ArrayList<ShortMutationId> delta = new ArrayList<>();
+ MutationSummary.difference(secondarySummary, initialSummary, delta);
+
+ delta.forEach(mutationId -> {
+ Mutation mutation = MutationJournal.instance.read(mutationId);
+ Preconditions.checkNotNull(mutation);
+ read.augment(mutation);
+ });
+ }
+
+ public void startLocalRead(TrackedRead.Id readId, ReadCommand command, ReplicaPlan.AbstractForRead<?, ?> replicaPlan, int[] summaryNodes, long expiresAtNanos)
+ {
+ synchronized (this)
+ {
+ if (!(state = state.startLocalRead(readId, promise, command, replicaPlan, summaryNodes, expiresAtNanos)).isReading())
+ return;
+ }
+
+ PartialTrackedRead read;
+ MutationSummary secondarySummary;
+
+ MutationSummary initialSummary = command.createMutationSummary(false);
+ ReadExecutionController controller = command.executionController(false);
+ try
+ {
+ read = command.beginTrackedRead(controller);
+ // Create another summary once initial data has been read fully. We do this to catch
+ // any mutations that may have arrived during initial read execution.
+ secondarySummary = command.createMutationSummary(true);
+ processDelta(read, initialSummary, secondarySummary);
+ }
+ catch (Exception e)
+ {
+ controller.close();
+ abort();
+ throw e;
+ }
+
+ synchronized (this)
+ {
+ state = state.receiveInProgressRead(read, secondarySummary);
+ }
+ }
+
+ private static void complete(AsyncPromise<TrackedDataResponse> promise, PartialTrackedRead read, ColumnFilter selection, ConsistencyLevel consistencyLevel, long expiresAtNanos)
+ {
+ Stage.READ.submit(() -> completeInternal(promise, read, selection, consistencyLevel, expiresAtNanos));
+ }
+
+ private static void completeInternal(AsyncPromise<TrackedDataResponse> promise, PartialTrackedRead read, ColumnFilter selection, ConsistencyLevel consistencyLevel, long expiresAtNanos)
+ {
+ try (PartialTrackedRead.CompletedRead completedRead = read.complete())
+ {
+ TrackedDataResponse response = TrackedDataResponse.create(completedRead.iterator(), selection);
+ TrackedRead<?, ?> followUp = completedRead.followupRead(consistencyLevel, expiresAtNanos);
+
+ if (followUp != null)
+ {
+ ReadCommand command = read.command();
+ followUp.future().addCallback((iterator, error) -> {
+ if (error != null)
+ {
+ promise.tryFailure(error);
+ return;
+ }
+ PartitionIterator previous = response.makeIterator(command);
+ TrackedDataResponse newResponse = TrackedDataResponse.create(PartitionIterators.concat(List.of(previous, iterator)), selection);
+ promise.trySuccess(newResponse);
+ });
+ }
+ else
+ {
+ promise.trySuccess(response);
+ }
+ }
+ catch (Exception e)
+ {
+ promise.tryFailure(e);
+ throw e;
+ }
+ finally
+ {
+ read.close();
+ }
+ }
+
+ synchronized void receiveSummary(InetAddressAndPort from, MutationSummary summary)
+ {
+ if (logger.isTraceEnabled())
+ logger.trace("Received summary {} from {}, for {}", summary, from, state);
+ state = state.receiveSummary(from, summary);
+ }
+
+ synchronized boolean acknowledgeSync(int syncId)
+ {
+ return (state = state.acknowledgeSync(syncId)).isCompleted();
+ }
+
+ synchronized boolean receiveMutations(List<Mutation> mutations)
+ {
+ return (state = state.receiveMutations(mutations)).isCompleted();
+ }
+
+ synchronized void abort()
+ {
+ state = state.abort();
+ }
+
+ boolean isTimedOutOrComplete(long nanoTime)
+ {
+ return state.isPurgeable(nanoTime);
+ }
+}
diff --git a/src/java/org/apache/cassandra/service/reads/tracked/TrackedLocalReads.java b/src/java/org/apache/cassandra/service/reads/tracked/TrackedLocalReads.java
new file mode 100644
index 0000000000..5c2417e6b5
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/reads/tracked/TrackedLocalReads.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.reads.tracked;
+
+import org.apache.cassandra.concurrent.ScheduledExecutorPlus;
+import org.apache.cassandra.concurrent.Shutdownable;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.*;
+import org.apache.cassandra.locator.InetAddressAndPort;
+import org.apache.cassandra.locator.ReplicaPlan;
+import org.apache.cassandra.locator.ReplicaPlans;
+import org.apache.cassandra.service.reads.SpeculativeRetryPolicy;
+import org.apache.cassandra.tcm.ClusterMetadata;
+import org.apache.cassandra.utils.Clock;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.ExecutorFactory.SimulatorSemantics.NORMAL;
+
+/**
+ * Since the read reconciliations don't use 2 way callbacks, a map of active reconciliations
+ * are maintained and expired here.
+ *
+ * Borrowed heavily from RequestCallbacks
+ */
+public class TrackedLocalReads implements Shutdownable
+{
+ private static final Logger logger = LoggerFactory.getLogger(TrackedLocalReads.class);
+
+ private final ConcurrentMap<TrackedRead.Id, TrackedLocalReadCoordinator> reads = new ConcurrentHashMap<>();
+ private final ScheduledExecutorPlus executor = executorFactory().scheduled("Reconciliation-Map-Reaper", NORMAL);
+
+ public TrackedLocalReads()
+ {
+ long expirationInterval = defaultExpirationInterval();
+ executor.scheduleWithFixedDelay(this::expire, expirationInterval, expirationInterval, NANOSECONDS);
+ }
+
+ private void expire()
+ {
+ long start = Clock.Global.nanoTime();
+ int n = 0;
+ for (Map.Entry<TrackedRead.Id, TrackedLocalReadCoordinator> entry : reads.entrySet())
+ {
+ TrackedLocalReadCoordinator read = entry.getValue();
+ if (read.isTimedOutOrComplete(start))
+ {
+ read.abort();
+ if (reads.remove(entry.getKey(), entry.getValue()))
+ n++;
+ }
+ }
+ if (n > 0)
+ logger.trace("Expired {} entries", n);
+ }
+
+ private TrackedLocalReadCoordinator getOrCreate(TrackedRead.Id id)
+ {
+ return reads.computeIfAbsent(id, TrackedLocalReadCoordinator::new);
+ }
+
+ public void receiveSummary(InetAddressAndPort from, TrackedSummaryResponse summary)
+ {
+ getOrCreate(summary.readId()).receiveSummary(from, summary.summary());
+ }
+
+ public TrackedLocalReadCoordinator beginRead(TrackedRead.Id readId, ClusterMetadata metadata, ReadCommand command, ConsistencyLevel consistencyLevel, int[] summaryNodes, long expiresAtNanos)
+ {
+ Keyspace keyspace = Keyspace.open(command.metadata().keyspace);
+ ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(command.metadata().id);
+ SpeculativeRetryPolicy retry = cfs.metadata().params.speculativeRetry;
+ ReplicaPlan.AbstractForRead<?, ?> replicaPlan;
+ if (command instanceof SinglePartitionReadCommand)
+ {
+ replicaPlan = ReplicaPlans.forRead(metadata,
+ keyspace,
+ ((SinglePartitionReadCommand) command).partitionKey().getToken(),
+ command.indexQueryPlan(),
+ consistencyLevel,
+ retry);
+ }
+ else
+ {
+ // TODO: confirm range we're reading doesn't span multiple replica sets
+ replicaPlan = ReplicaPlans.forRangeRead(keyspace,
+ command.indexQueryPlan(),
+ consistencyLevel,
+ command.dataRange().keyRange(),
+ 1);
+ }
+ // TODO: confirm all summaryNodes are present in the replica plan
+
+ TrackedLocalReadCoordinator coordinator = getOrCreate(readId);
+ coordinator.startLocalRead(readId, command, replicaPlan, summaryNodes, expiresAtNanos);
+ return coordinator;
+ }
+
+ public void acknowledgeSync(TrackedRead.Id readId, int syncId)
+ {
+ TrackedLocalReadCoordinator read = reads.get(readId);
+ if (read == null)
+ return;
+
+ if (read.acknowledgeSync(syncId))
+ reads.remove(readId);
+ }
+
+ public boolean receiveMutations(TrackedRead.Id readId, int syncId, List<Mutation> mutations)
+ {
+ TrackedLocalReadCoordinator read = reads.get(readId);
+ if (read == null)
+ return false;
+
+ read.receiveMutations(mutations);
+ if (read.acknowledgeSync(syncId))
+ reads.remove(readId);
+ return true;
+ }
+
+ public static long defaultExpirationInterval()
+ {
+ return DatabaseDescriptor.getMinRpcTimeout(NANOSECONDS) / 2;
+ }
+
+ @Override
+ public void shutdown()
+ {
+ executor.shutdown();
+ }
... 5541 lines suppressed ...
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org