frankgh commented on code in PR #4708: URL: https://github.com/apache/cassandra/pull/4708#discussion_r3500914682
########## src/java/org/apache/cassandra/locator/CoordinationPlan.java: ########## @@ -0,0 +1,321 @@ +/* + * 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.locator; + +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; + +import javax.annotation.Nullable; + +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.exceptions.UnavailableException; +import org.apache.cassandra.index.Index; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.TableId; +import org.apache.cassandra.service.reads.ReadCoordinator; +import org.apache.cassandra.service.reads.SpeculativeRetryPolicy; +import org.apache.cassandra.tcm.ClusterMetadata; + +/** + * Ties together replica selection and response tracking for a single operation. + * + * This immutable container ensures that the replica plan (who to contact) and + * the response tracker (how to determine success) are created atomically by the + * replication strategy, consulting the same state. This is particularly important + * for strategies that have per-range state (e.g., failover state in SRS) where + * the replica selection and quorum requirements must be consistent. + * + * The separation between ReplicaPlan and ResponseTracker allows: + * - ReplicaPlan to focus on replica topology and selection + * - ResponseTracker to encapsulate completion logic + * - Replication strategies to customize both consistently Review Comment: NIT, can we have proper javadoc syntax here? ``` * <p>The separation between ReplicaPlan and ResponseTracker allows: * <ul> * <li>ReplicaPlan to focus on replica topology and selection * <li>ResponseTracker to encapsulate completion logic * <li>Replication strategies to customize both consistently * </ul> ``` ########## src/java/org/apache/cassandra/locator/CoordinationPlan.java: ########## @@ -0,0 +1,321 @@ +/* + * 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.locator; + +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; + +import javax.annotation.Nullable; + +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.exceptions.UnavailableException; +import org.apache.cassandra.index.Index; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.TableId; +import org.apache.cassandra.service.reads.ReadCoordinator; +import org.apache.cassandra.service.reads.SpeculativeRetryPolicy; +import org.apache.cassandra.tcm.ClusterMetadata; + +/** + * Ties together replica selection and response tracking for a single operation. + * + * This immutable container ensures that the replica plan (who to contact) and + * the response tracker (how to determine success) are created atomically by the + * replication strategy, consulting the same state. This is particularly important + * for strategies that have per-range state (e.g., failover state in SRS) where + * the replica selection and quorum requirements must be consistent. + * + * The separation between ReplicaPlan and ResponseTracker allows: + * - ReplicaPlan to focus on replica topology and selection + * - ResponseTracker to encapsulate completion logic + * - Replication strategies to customize both consistently + * + * No polymorphism is needed at this level - the variation is captured in the + * ResponseTracker implementations. This is just a typed tuple ensuring the + * two pieces travel together. + * + * @param <P> the type of ReplicaPlan (ForRead, ForWrite, ForPaxosWrite, etc.) + */ +public abstract class CoordinationPlan<E extends Endpoints<E>, P extends ReplicaPlan<E, P>> +{ + // TODO (now): consolidate callback Condition instances into this. Replace all condition await calls with calls to this. + private final ResponseTracker responses; + + /** + * Create a coordination plan. + * + * @param responses the response tracker for determining completion/success + */ + public CoordinationPlan(ResponseTracker responses) + { + if (responses == null) + throw new IllegalArgumentException("tracker cannot be null"); + + this.responses = responses; Review Comment: NIT, should this be a NPE instead? ``` this.responses = Objects.requireNonNull(responses, "tracker cannot be null"); ``` ########## src/java/org/apache/cassandra/locator/CoordinationPlan.java: ########## @@ -0,0 +1,321 @@ +/* + * 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.locator; + +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; + +import javax.annotation.Nullable; + +import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.exceptions.UnavailableException; +import org.apache.cassandra.index.Index; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.TableId; +import org.apache.cassandra.service.reads.ReadCoordinator; +import org.apache.cassandra.service.reads.SpeculativeRetryPolicy; +import org.apache.cassandra.tcm.ClusterMetadata; + +/** + * Ties together replica selection and response tracking for a single operation. + * + * This immutable container ensures that the replica plan (who to contact) and + * the response tracker (how to determine success) are created atomically by the + * replication strategy, consulting the same state. This is particularly important + * for strategies that have per-range state (e.g., failover state in SRS) where + * the replica selection and quorum requirements must be consistent. + * + * The separation between ReplicaPlan and ResponseTracker allows: + * - ReplicaPlan to focus on replica topology and selection + * - ResponseTracker to encapsulate completion logic + * - Replication strategies to customize both consistently + * + * No polymorphism is needed at this level - the variation is captured in the + * ResponseTracker implementations. This is just a typed tuple ensuring the + * two pieces travel together. + * + * @param <P> the type of ReplicaPlan (ForRead, ForWrite, ForPaxosWrite, etc.) + */ +public abstract class CoordinationPlan<E extends Endpoints<E>, P extends ReplicaPlan<E, P>> +{ + // TODO (now): consolidate callback Condition instances into this. Replace all condition await calls with calls to this. + private final ResponseTracker responses; + + /** + * Create a coordination plan. + * + * @param responses the response tracker for determining completion/success + */ + public CoordinationPlan(ResponseTracker responses) + { + if (responses == null) + throw new IllegalArgumentException("tracker cannot be null"); + + this.responses = responses; + } + + public ConsistencyLevel consistencyLevel() + { + return replicas().consistencyLevel(); + } + + public AbstractReplicationStrategy replicationStrategy() + { + return replicas().replicationStrategy(); + } + + public abstract P replicas(); + + /** + * The response tracker for determining completion/success. + * + * The tracker encapsulates the logic for: + * - Recording responses and failures + * - Determining when the operation is complete + * - Checking if the operation succeeded + * + * @return the response tracker + */ + public ResponseTracker responses() + { + return responses; + } + + @Override + public String toString() + { + return String.format("CoordinationPlan[replicaPlan=%s, tracker=%s]", replicas(), responses.getClass().getSimpleName()); + } + + /** + * Extended plan including source cluster metadata and ideal coordination plan. + */ + public static class ForWrite extends CoordinationPlan<EndpointsForToken, ReplicaPlan.ForWrite> + { + private final ReplicaPlan.ForWrite replicas; + + public ForWrite(ReplicaPlan.ForWrite replicas, ResponseTracker responses) + { + super(responses); + this.replicas = replicas; + } + + @Override + public ReplicaPlan.ForWrite replicas() + { + return replicas; + } + } + + public static class ForWriteWithIdeal extends CoordinationPlan.ForWrite + { + public final ClusterMetadata metadata; Review Comment: unused and can be removed unless we have plans to use `ClusterMetadata` in the future ########## src/java/org/apache/cassandra/batchlog/BatchlogManager.java: ########## @@ -635,26 +630,13 @@ private static ReplayWriteResponseHandler<Mutation> sendSingleReplayMutation(fin } } - ReplayWriteResponseHandler<Mutation> handler = new ReplayWriteResponseHandler<>(replicaPlan, mutation, Dispatcher.RequestTime.forImmediateExecution()); + ReplayWriteResponseHandler<Mutation> handler = new ReplayWriteResponseHandler<>(replayPlan, () -> mutation, Dispatcher.RequestTime.forImmediateExecution()); Review Comment: `Mutation` already implements `Supplier<Mutation>`. Do we need the change here? or should we preserve the original? ``` ReplayWriteResponseHandler<Mutation> handler = new ReplayWriteResponseHandler<>(replayPlan, mutation, Dispatcher.RequestTime.forImmediateExecution()); ``` ########## src/java/org/apache/cassandra/locator/ReplicaPlans.java: ########## @@ -287,6 +278,35 @@ public static ReplicaPlan.ForWrite forLocalBatchlogWrite() return forWrite(systemKeyspace, ConsistencyLevel.ONE, (cm) -> liveAndDown, (cm) -> true, writeAll); } + /** + * Create a replica plan for replaying a mutation from the batchlog. + * + * When recovering failed batches, mutations are replayed to live remote replicas only + * (local replica is handled separately by the caller). + * + * @param metadata the cluster metadata + * @param keyspace the keyspace + * @param token the token for the mutation + * @return replica plan targeting live remote replicas with CL.ONE + */ + public static ReplicaPlan.ForWrite forReplayMutation(ClusterMetadata metadata, Keyspace keyspace, Token token) + { + ReplicaLayout.ForTokenWrite liveAndDown = ReplicaLayout.forTokenWriteLiveAndDown(metadata, keyspace.getMetadata(), token); + Replicas.temporaryAssertFull(liveAndDown.all()); // TODO in CASSANDRA-14549 + + Replica selfReplica = liveAndDown.all().selfIfPresent(); + ReplicaLayout.ForTokenWrite liveRemoteOnly = liveAndDown.filter(r -> FailureDetector.isReplicaAlive.test(r) && r != selfReplica); + + return new ReplicaPlan.ForWrite(keyspace, keyspace.getReplicationStrategy(), + ConsistencyLevel.ONE, + liveRemoteOnly.pending(), + liveRemoteOnly.all(), + liveRemoteOnly.all(), + liveRemoteOnly.all(), Review Comment: NIT: extract `liveRemoteOnly.all()` into a local variable: ``` EndpointsForToken allLiveRemoteOnly = liveRemoteOnly.all(); return new ReplicaPlan.ForWrite(keyspace, keyspace.getReplicationStrategy(), ConsistencyLevel.ONE, liveRemoteOnly.pending(), allLiveRemoteOnly, allLiveRemoteOnly, allLiveRemoteOnly, ``` ########## src/java/org/apache/cassandra/locator/WriteResponseTracker.java: ########## @@ -0,0 +1,237 @@ +/* + * 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.locator; + +import java.util.function.Predicate; + +import org.apache.cassandra.exceptions.RequestFailureReason; + +/** + * Response tracker for writes with pending replicas using the double count model. + * <p> + * Two requirements must be satisfied for success: + * <ol> + * <li>Committed replicas must satisfy base CL: {@code committedSuccesses >= baseBlockFor}</li> + * <li>Total replicas (committed + pending) must satisfy: {@code totalSuccesses >= totalBlockFor}</li> + * </ol> + * <p> + * This ensures both consistency (committed replicas have the data) and bootstrap safety + * (pending replicas also receive the write). + * <p> + * Implemented by composing two {@link SimpleResponseTracker}s: + * <ul> + * <li>committedTracker: tracks responses from committed replicas only</li> + * <li>totalTracker: tracks responses from all replicas (committed + pending)</li> + * </ul> + * <p> + * Thread-safe through delegation to thread-safe SimpleResponseTrackers. + */ +public class WriteResponseTracker implements ResponseTracker +{ + private final SimpleResponseTracker natural; + private final SimpleResponseTracker total; + + /** + * Create a write response tracker with the double count model. + * + * @param baseBlockFor number of committed replica responses required for CL + * @param totalBlockFor total responses required (baseBlockFor + pending count) + * @param committedReplicas number of committed replicas available + * @param pendingReplicas number of pending replicas available + * @param isPending predicate to determine if an endpoint is pending + */ + public WriteResponseTracker(int baseBlockFor, + int totalBlockFor, + int committedReplicas, + int pendingReplicas, + Predicate<InetAddressAndPort> isPending) + { + this(baseBlockFor, totalBlockFor, committedReplicas, pendingReplicas, isPending, null); + } + + /** + * Create a write response tracker with the double count model and a filter. + * + * @param naturalBlockFor number of committed replica responses required for CL + * @param totalBlockFor total responses required (naturalBlockFor + pending count) + * @param naturalReplicas number of committed replicas available + * @param pendingReplicas number of pending replicas available + * @param isPending predicate to determine if an endpoint is pending + * @param filter predicate to filter which responses count (e.g., InOurDc for LOCAL_QUORUM) + */ + public WriteResponseTracker(int naturalBlockFor, + int totalBlockFor, + int naturalReplicas, + int pendingReplicas, + Predicate<InetAddressAndPort> isPending, + Predicate<InetAddressAndPort> filter) + { + if (naturalBlockFor < 0) + throw new IllegalArgumentException("naturalBlockFor must be non-negative: " + naturalBlockFor); + if (totalBlockFor < naturalBlockFor) + throw new IllegalArgumentException("totalBlockFor (" + totalBlockFor + ") must be >= naturalBlockFor (" + naturalBlockFor + ")"); + if (naturalReplicas < 0) + throw new IllegalArgumentException("naturalReplicas must be non-negative: " + naturalReplicas); + if (pendingReplicas < 0) + throw new IllegalArgumentException("pendingReplicas must be non-negative: " + pendingReplicas); + if (naturalBlockFor > naturalReplicas) + throw new IllegalArgumentException("naturalBlockFor (" + naturalBlockFor + ") cannot exceed naturalReplicas (" + naturalReplicas + ")"); + if (totalBlockFor > naturalReplicas + pendingReplicas) + throw new IllegalArgumentException("totalBlockFor (" + totalBlockFor + ") cannot exceed total replicas (" + (naturalReplicas + pendingReplicas) + ")"); + if (isPending == null) + throw new IllegalArgumentException("isPending predicate cannot be null"); + + Predicate<InetAddressAndPort> naturalFilter = isPending.negate(); + if (filter != null) + naturalFilter = naturalFilter.and(filter); + + this.natural = new SimpleResponseTracker(naturalBlockFor, naturalReplicas, naturalFilter); + this.total = new SimpleResponseTracker(totalBlockFor, naturalReplicas + pendingReplicas, filter); + } + + private WriteResponseTracker(SimpleResponseTracker natural, SimpleResponseTracker total) + { + this.natural = natural; + this.total = total; + } + + @Override + public void onResponse(InetAddressAndPort from) + { + natural.onResponse(from); + total.onResponse(from); + } + + @Override + public void onFailure(InetAddressAndPort from, RequestFailureReason reason) + { + natural.onFailure(from, reason); + total.onFailure(from, reason); + } + + @Override + public boolean isComplete() + { + // Early failure if either tracker fails + if (natural.isComplete() && !natural.isSuccessful()) + return true; + if (total.isComplete() && !total.isSuccessful()) + return true; + + // Success requires both to succeed + return natural.isSuccessful() && total.isSuccessful(); + } + + @Override + public boolean isSuccessful() + { + return natural.isSuccessful() && total.isSuccessful(); + } + + @Override + public int required() + { + // Return total requirement for error messages + return total.required(); + } + + @Override + public int received() + { + // Return total successes for error messages + return total.received(); + } + + @Override + public int failures() + { + // Return total failures for error messages + return total.failures(); + } + + @Override + public boolean countsTowardQuorum(InetAddressAndPort from) + { + return total.countsTowardQuorum(from); + } + + public int committedReceived() + { + return natural.received(); + } + + public int pendingReceived() Review Comment: This is not really atomic. Thread 1 calls onResponse, and natural updates `responses` , then thread 2 calls `pendingReceived` (or `pendingFailures`) and reads updated value from both `total` and `natural`. thread 1 has not yet updated `responses` of total. Since we are only using this for test, I think we should at least call it out, also maybe not make it public unless we need it ########## src/java/org/apache/cassandra/locator/AbstractReplicationStrategy.java: ########## @@ -437,4 +459,362 @@ public RangesAtEndpoint getLocalRanges(ClusterMetadata cm) return newRanges; } } + + protected CoordinationPlan.ForWrite planForWriteInternal(ClusterMetadata metadata, + Keyspace keyspace, + ConsistencyLevel consistencyLevel, + Function<ClusterMetadata, ReplicaLayout.ForTokenWrite> liveAndDown, + ReplicaPlans.Selector selector) + { + ReplicaPlan.ForWrite plan = ReplicaPlans.forWrite(metadata, keyspace, consistencyLevel, liveAndDown, selector); + ResponseTracker tracker = createTrackerForWrite(consistencyLevel, plan, plan.pending, metadata); + return new CoordinationPlan.ForWrite(plan, tracker); + } + + protected ReplicaPlan.ForWrite createReplicaPlanForWrite(ClusterMetadata metadata, + Keyspace keyspace, + ConsistencyLevel consistencyLevel, + Function<ClusterMetadata, ReplicaLayout.ForTokenWrite> liveAndDown, + ReplicaPlans.Selector selector) + { + return ReplicaPlans.forWrite(metadata, keyspace, consistencyLevel, liveAndDown, selector); + } + + public CoordinationPlan.ForWriteWithIdeal planForWrite(ClusterMetadata metadata, + Keyspace keyspace, + ConsistencyLevel consistencyLevel, + Function<ClusterMetadata, ReplicaLayout.ForTokenWrite> liveAndDown, + ReplicaPlans.Selector selector) + { + ReplicaPlan.ForWrite plan = createReplicaPlanForWrite(metadata, keyspace, consistencyLevel, liveAndDown, selector); + ResponseTracker tracker = createTrackerForWrite(consistencyLevel, plan, plan.pending, metadata); + + CoordinationPlan.ForWrite ideal = null; + ConsistencyLevel idealCL = DatabaseDescriptor.getIdealConsistencyLevel(); + if (idealCL != null) + { + if (idealCL == consistencyLevel) + { + ideal = new CoordinationPlan.ForWrite(plan, tracker); + } + else + { + ideal = new CoordinationPlan.ForWrite(createReplicaPlanForWrite(metadata, keyspace, idealCL, liveAndDown, selector), + createTrackerForWrite(idealCL, plan, plan.pending, metadata)); + } + } + + return new CoordinationPlan.ForWriteWithIdeal(metadata, plan, tracker, ideal); + } + + public CoordinationPlan.ForWriteWithIdeal planForWrite(ClusterMetadata metadata, + Keyspace keyspace, + ConsistencyLevel consistencyLevel, + Token token, + ReplicaPlans.Selector selector) + { + return planForWrite(metadata, keyspace, consistencyLevel, + (newClusterMetadata) -> ReplicaLayout.forTokenWriteLiveAndDown(newClusterMetadata, keyspace, token), selector); + } + + /** + * Create coordination plan for forwarding a counter write to the leader replica. + * + * In cases where the original coordinator is not a replica of the counter key, the counter + * mutation is forwarded to a leader replica that will coordinate the actual counter update. + */ + public CoordinationPlan.ForWrite planForForwardingCounterWrite(ClusterMetadata metadata, + Keyspace keyspace, + Token token, + Function<ClusterMetadata, Replica> replicaSupplier) + { + ReplicaPlan.ForWrite plan = ReplicaPlans.forSingleReplicaWrite(metadata, keyspace, token, replicaSupplier); + ResponseTracker tracker = createTrackerForWrite(plan.consistencyLevel(), plan, plan.pending, metadata); + + return new CoordinationPlan.ForWriteWithIdeal(metadata, plan, tracker, null); + } + + /** + * Create coordination plan for replaying a mutation from the batchlog. + * + * When recovering failed batches, mutations are replayed to remote replicas only + * (local replica is handled separately). This method creates a coordination plan + * targeting live remote replicas with CL.ONE. + */ + public CoordinationPlan.ForWriteWithIdeal planForReplayMutation(ClusterMetadata metadata, + Keyspace keyspace, + Token token) + { + Preconditions.checkState(!replicationType.isTracked(), "Batch replay not supported with tracked keyspaces"); + + ReplicaPlan.ForWrite plan = ReplicaPlans.forReplayMutation(metadata, keyspace, token); + ResponseTracker tracker = createTrackerForWrite(plan.consistencyLevel(), plan, plan.pending, metadata); + + return new CoordinationPlan.ForWriteWithIdeal(metadata, plan, tracker, null); + } + + /** + * Create coordination plan for a single-partition token read. + */ + public CoordinationPlan.ForTokenRead planForTokenRead(ClusterMetadata metadata, + Keyspace keyspace, + TableId tableId, + Token token, + @Nullable Index.QueryPlan indexQueryPlan, + ConsistencyLevel consistencyLevel, + SpeculativeRetryPolicy retry, + ReadCoordinator coordinator) + { + ReplicaPlan.ForTokenRead plan = ReplicaPlans.forRead(metadata, keyspace, tableId, token, indexQueryPlan, consistencyLevel, retry, coordinator); + ReplicaPlan.SharedForTokenRead shared = ReplicaPlan.shared(plan); + ResponseTracker tracker = createTrackerForRead(plan); + return new CoordinationPlan.ForTokenRead(shared, tracker); + } + + /** + * Create coordination plan for a range read. + */ + public CoordinationPlan.ForRangeRead planForRangeRead(ClusterMetadata metadata, + Keyspace keyspace, + TableId tableId, + @Nullable Index.QueryPlan indexQueryPlan, + ConsistencyLevel consistencyLevel, + AbstractBounds<PartitionPosition> range, + int vnodeCount) + { + ReplicaPlan.ForRangeRead plan = ReplicaPlans.forRangeRead(metadata, keyspace, tableId, indexQueryPlan, consistencyLevel, range, vnodeCount, true); + ReplicaPlan.SharedForRangeRead shared = ReplicaPlan.shared(plan); + ResponseTracker tracker = createTrackerForRead(plan); + return new CoordinationPlan.ForRangeRead(shared, tracker); + } + + /** + * Attempt to merge two adjacent range read coordination plans into one. + * + * If the two plans share enough live endpoints to satisfy the consistency level + * and the merge is worthwhile returns a merged plan otherwise returns null. + */ + public CoordinationPlan.ForRangeRead maybeMergeRangeReads(ClusterMetadata metadata, + Keyspace keyspace, + TableId tableId, + ConsistencyLevel consistencyLevel, + ReplicaPlan.ForRangeRead left, + ReplicaPlan.ForRangeRead right) + { + ReplicaPlan.ForRangeRead merged = ReplicaPlans.maybeMerge(metadata, keyspace, tableId, consistencyLevel, left, right); + if (merged == null) + return null; + + ReplicaPlan.SharedForRangeRead shared = ReplicaPlan.shared(merged); + ResponseTracker tracker = createTrackerForRead(merged); + return new CoordinationPlan.ForRangeRead(shared, tracker); + } + + /** + * Create coordination plan for a full range read + */ + public CoordinationPlan.ForRangeRead planForFullRangeRead(Keyspace keyspace, + ConsistencyLevel consistencyLevel, + AbstractBounds<PartitionPosition> range, + Set<InetAddressAndPort> endpointsToContact, + int vnodeCount) + { + ReplicaPlan.ForRangeRead plan = ReplicaPlans.forFullRangeRead(keyspace, consistencyLevel, range, endpointsToContact, vnodeCount); + ReplicaPlan.SharedForRangeRead shared = ReplicaPlan.shared(plan); + ResponseTracker tracker = createTrackerForRead(plan); + return new CoordinationPlan.ForRangeRead(shared, tracker); + } + + /** + * Create coordination plan for a single-replica token read. + */ + public CoordinationPlan.ForTokenRead planForSingleReplicaTokenRead(Keyspace keyspace, Token token, Replica replica) + { + ReplicaPlan.ForTokenRead plan = ReplicaPlans.forSingleReplicaRead(keyspace, token, replica); + ReplicaPlan.SharedForTokenRead shared = ReplicaPlan.shared(plan); + ResponseTracker tracker = createTrackerForRead(plan); + return new CoordinationPlan.ForTokenRead(shared, tracker); + } + + /** + * Create coordination plan for a single-replica range read. + * + * Used by short read protection to fetch additional partitions from a + * specific replica. blockFor=1, totalReplicas=1. + */ + public CoordinationPlan.ForRangeRead planForSingleReplicaRangeRead(Keyspace keyspace, + AbstractBounds<PartitionPosition> range, + Replica replica, + int vnodeCount) + { + ReplicaPlan.ForRangeRead plan = ReplicaPlans.forSingleReplicaRead(keyspace, range, replica, vnodeCount); + ReplicaPlan.SharedForRangeRead shared = ReplicaPlan.shared(plan); + ResponseTracker tracker = createTrackerForRead(plan); + return new CoordinationPlan.ForRangeRead(shared, tracker); + } + + /** + * Create ResponseTracker for read operation. + */ + public <E extends Endpoints<E>, P extends ReplicaPlan.ForRead<E, P>> ResponseTracker createTrackerForRead(P plan) + { + int blockFor = plan.readQuorum(); + + // Use candidates.size() for totalReplicas to allow for speculation + // (speculation can contact additional candidates beyond initial contacts) + int totalReplicas = plan.readCandidates().size(); + + return new SimpleResponseTracker(blockFor, totalReplicas); + } + + public Paxos.Participants paxosParticipants(ClusterMetadata metadata, + TableMetadata table, + Token token, + ConsistencyLevel consistencyForConsensus, + Predicate<Replica> isReplicaAlive) + { + + KeyspaceMetadata keyspaceMetadata = metadata.schema.getKeyspaceMetadata(table.keyspace); + // MetaStrategy distributes the entire keyspace to all replicas. In addition, its tables (currently only + // the dist log table) don't use the globally configured partitioner. For these reasons we don't lookup the + // replicas using the supplied token as this can actually be of the incorrect type (for example when + // performing Paxos repair). + final Token actualToken = table.partitioner == MetaStrategy.partitioner ? MetaStrategy.entireRange.right : token; + ReplicaLayout.ForTokenWrite all = forTokenWriteLiveAndDown(keyspaceMetadata, actualToken); + ReplicaLayout.ForTokenWrite electorate = consistencyForConsensus.isDatacenterLocal() + ? all.filter(InOurDc.replicas()) : all; + + EndpointsForToken live = all.all().filter(isReplicaAlive); + return new Paxos.Participants(metadata.epoch, Keyspace.open(table.keyspace), consistencyForConsensus, all, electorate, live, + (cm) -> Paxos.Participants.get(cm, table, actualToken, consistencyForConsensus)); + } + + /** + * Create ResponseTracker for write operation based on consistency level. + */ + @VisibleForTesting + public ResponseTracker createTrackerForWrite(ConsistencyLevel cl, ReplicaPlan.ForWrite plan, Endpoints<?> pending, ClusterMetadata metadata) + { + switch (cl) + { + case ANY: + case ONE: + case TWO: + case THREE: + case QUORUM: + case ALL: + int totalContacts = plan.contacts().size(); + if (pending.isEmpty()) + { + int blockFor = cl.blockFor(this); + return new SimpleResponseTracker(blockFor, totalContacts); + } + else + { + // Check if double count model applies (some CLs like ANY don't add pending) + int baseBlockFor = cl.blockFor(this); + int totalBlockFor = cl.blockForWrite(this, pending); + + // If totalBlockFor == baseBlockFor, no double-count needed (e.g., ANY) + if (totalBlockFor == baseBlockFor) + return new SimpleResponseTracker(baseBlockFor, totalContacts); + + // Double count model: committed must satisfy base CL, total must include pending + int pendingReplicas = pending.size(); + // contacts() includes both committed and pending replicas + int committedReplicas = totalContacts - pendingReplicas; + return new WriteResponseTracker(baseBlockFor, totalBlockFor, + committedReplicas, pendingReplicas, + endpoint -> pending.endpoints().contains(endpoint)); + } Review Comment: Can be simplified to : ``` int totalContacts = plan.contacts().size(); int baseBlockFor = cl.blockFor(this); int totalBlockFor = pending.isEmpty() ? baseBlockFor : cl.blockForWrite(this, pending); // Check if double count model applies (some CLs like ANY don't add pending) // If totalBlockFor == baseBlockFor, no double-count needed (e.g., ANY) if (totalBlockFor == baseBlockFor) return new SimpleResponseTracker(baseBlockFor, totalContacts); // Double count model: natural must satisfy base CL, total must include pending int pendingReplicas = pending.size(); // contacts() includes both natural and pending replicas int naturalReplicas = totalContacts - pendingReplicas; return new WriteResponseTracker(baseBlockFor, totalBlockFor, naturalReplicas, pendingReplicas, endpoint -> pending.endpoints().contains(endpoint)); ``` ########## src/java/org/apache/cassandra/locator/AbstractReplicationStrategy.java: ########## @@ -437,4 +459,362 @@ public RangesAtEndpoint getLocalRanges(ClusterMetadata cm) return newRanges; } } + + protected CoordinationPlan.ForWrite planForWriteInternal(ClusterMetadata metadata, + Keyspace keyspace, + ConsistencyLevel consistencyLevel, + Function<ClusterMetadata, ReplicaLayout.ForTokenWrite> liveAndDown, + ReplicaPlans.Selector selector) + { + ReplicaPlan.ForWrite plan = ReplicaPlans.forWrite(metadata, keyspace, consistencyLevel, liveAndDown, selector); + ResponseTracker tracker = createTrackerForWrite(consistencyLevel, plan, plan.pending, metadata); + return new CoordinationPlan.ForWrite(plan, tracker); + } + + protected ReplicaPlan.ForWrite createReplicaPlanForWrite(ClusterMetadata metadata, + Keyspace keyspace, + ConsistencyLevel consistencyLevel, + Function<ClusterMetadata, ReplicaLayout.ForTokenWrite> liveAndDown, + ReplicaPlans.Selector selector) + { + return ReplicaPlans.forWrite(metadata, keyspace, consistencyLevel, liveAndDown, selector); + } + + public CoordinationPlan.ForWriteWithIdeal planForWrite(ClusterMetadata metadata, + Keyspace keyspace, + ConsistencyLevel consistencyLevel, + Function<ClusterMetadata, ReplicaLayout.ForTokenWrite> liveAndDown, + ReplicaPlans.Selector selector) + { + ReplicaPlan.ForWrite plan = createReplicaPlanForWrite(metadata, keyspace, consistencyLevel, liveAndDown, selector); + ResponseTracker tracker = createTrackerForWrite(consistencyLevel, plan, plan.pending, metadata); + + CoordinationPlan.ForWrite ideal = null; + ConsistencyLevel idealCL = DatabaseDescriptor.getIdealConsistencyLevel(); + if (idealCL != null) + { + if (idealCL == consistencyLevel) + { + ideal = new CoordinationPlan.ForWrite(plan, tracker); + } + else + { + ideal = new CoordinationPlan.ForWrite(createReplicaPlanForWrite(metadata, keyspace, idealCL, liveAndDown, selector), + createTrackerForWrite(idealCL, plan, plan.pending, metadata)); + } + } + + return new CoordinationPlan.ForWriteWithIdeal(metadata, plan, tracker, ideal); + } + + public CoordinationPlan.ForWriteWithIdeal planForWrite(ClusterMetadata metadata, + Keyspace keyspace, + ConsistencyLevel consistencyLevel, + Token token, + ReplicaPlans.Selector selector) + { + return planForWrite(metadata, keyspace, consistencyLevel, + (newClusterMetadata) -> ReplicaLayout.forTokenWriteLiveAndDown(newClusterMetadata, keyspace, token), selector); + } + + /** + * Create coordination plan for forwarding a counter write to the leader replica. + * + * In cases where the original coordinator is not a replica of the counter key, the counter + * mutation is forwarded to a leader replica that will coordinate the actual counter update. + */ + public CoordinationPlan.ForWrite planForForwardingCounterWrite(ClusterMetadata metadata, + Keyspace keyspace, + Token token, + Function<ClusterMetadata, Replica> replicaSupplier) + { + ReplicaPlan.ForWrite plan = ReplicaPlans.forSingleReplicaWrite(metadata, keyspace, token, replicaSupplier); + ResponseTracker tracker = createTrackerForWrite(plan.consistencyLevel(), plan, plan.pending, metadata); + + return new CoordinationPlan.ForWriteWithIdeal(metadata, plan, tracker, null); + } + + /** + * Create coordination plan for replaying a mutation from the batchlog. + * + * When recovering failed batches, mutations are replayed to remote replicas only + * (local replica is handled separately). This method creates a coordination plan + * targeting live remote replicas with CL.ONE. + */ + public CoordinationPlan.ForWriteWithIdeal planForReplayMutation(ClusterMetadata metadata, + Keyspace keyspace, + Token token) + { + Preconditions.checkState(!replicationType.isTracked(), "Batch replay not supported with tracked keyspaces"); + + ReplicaPlan.ForWrite plan = ReplicaPlans.forReplayMutation(metadata, keyspace, token); + ResponseTracker tracker = createTrackerForWrite(plan.consistencyLevel(), plan, plan.pending, metadata); + + return new CoordinationPlan.ForWriteWithIdeal(metadata, plan, tracker, null); + } + + /** + * Create coordination plan for a single-partition token read. + */ + public CoordinationPlan.ForTokenRead planForTokenRead(ClusterMetadata metadata, + Keyspace keyspace, + TableId tableId, + Token token, + @Nullable Index.QueryPlan indexQueryPlan, + ConsistencyLevel consistencyLevel, + SpeculativeRetryPolicy retry, + ReadCoordinator coordinator) + { + ReplicaPlan.ForTokenRead plan = ReplicaPlans.forRead(metadata, keyspace, tableId, token, indexQueryPlan, consistencyLevel, retry, coordinator); + ReplicaPlan.SharedForTokenRead shared = ReplicaPlan.shared(plan); + ResponseTracker tracker = createTrackerForRead(plan); + return new CoordinationPlan.ForTokenRead(shared, tracker); + } + + /** + * Create coordination plan for a range read. + */ + public CoordinationPlan.ForRangeRead planForRangeRead(ClusterMetadata metadata, + Keyspace keyspace, + TableId tableId, + @Nullable Index.QueryPlan indexQueryPlan, + ConsistencyLevel consistencyLevel, + AbstractBounds<PartitionPosition> range, + int vnodeCount) + { + ReplicaPlan.ForRangeRead plan = ReplicaPlans.forRangeRead(metadata, keyspace, tableId, indexQueryPlan, consistencyLevel, range, vnodeCount, true); + ReplicaPlan.SharedForRangeRead shared = ReplicaPlan.shared(plan); + ResponseTracker tracker = createTrackerForRead(plan); + return new CoordinationPlan.ForRangeRead(shared, tracker); + } + + /** + * Attempt to merge two adjacent range read coordination plans into one. + * + * If the two plans share enough live endpoints to satisfy the consistency level + * and the merge is worthwhile returns a merged plan otherwise returns null. + */ + public CoordinationPlan.ForRangeRead maybeMergeRangeReads(ClusterMetadata metadata, + Keyspace keyspace, + TableId tableId, + ConsistencyLevel consistencyLevel, + ReplicaPlan.ForRangeRead left, + ReplicaPlan.ForRangeRead right) + { + ReplicaPlan.ForRangeRead merged = ReplicaPlans.maybeMerge(metadata, keyspace, tableId, consistencyLevel, left, right); + if (merged == null) + return null; + + ReplicaPlan.SharedForRangeRead shared = ReplicaPlan.shared(merged); + ResponseTracker tracker = createTrackerForRead(merged); + return new CoordinationPlan.ForRangeRead(shared, tracker); + } + + /** + * Create coordination plan for a full range read + */ + public CoordinationPlan.ForRangeRead planForFullRangeRead(Keyspace keyspace, + ConsistencyLevel consistencyLevel, + AbstractBounds<PartitionPosition> range, + Set<InetAddressAndPort> endpointsToContact, + int vnodeCount) + { + ReplicaPlan.ForRangeRead plan = ReplicaPlans.forFullRangeRead(keyspace, consistencyLevel, range, endpointsToContact, vnodeCount); + ReplicaPlan.SharedForRangeRead shared = ReplicaPlan.shared(plan); + ResponseTracker tracker = createTrackerForRead(plan); + return new CoordinationPlan.ForRangeRead(shared, tracker); + } + + /** + * Create coordination plan for a single-replica token read. + */ + public CoordinationPlan.ForTokenRead planForSingleReplicaTokenRead(Keyspace keyspace, Token token, Replica replica) + { + ReplicaPlan.ForTokenRead plan = ReplicaPlans.forSingleReplicaRead(keyspace, token, replica); + ReplicaPlan.SharedForTokenRead shared = ReplicaPlan.shared(plan); + ResponseTracker tracker = createTrackerForRead(plan); + return new CoordinationPlan.ForTokenRead(shared, tracker); + } + + /** + * Create coordination plan for a single-replica range read. + * + * Used by short read protection to fetch additional partitions from a + * specific replica. blockFor=1, totalReplicas=1. + */ + public CoordinationPlan.ForRangeRead planForSingleReplicaRangeRead(Keyspace keyspace, + AbstractBounds<PartitionPosition> range, + Replica replica, + int vnodeCount) + { + ReplicaPlan.ForRangeRead plan = ReplicaPlans.forSingleReplicaRead(keyspace, range, replica, vnodeCount); + ReplicaPlan.SharedForRangeRead shared = ReplicaPlan.shared(plan); + ResponseTracker tracker = createTrackerForRead(plan); + return new CoordinationPlan.ForRangeRead(shared, tracker); + } + + /** + * Create ResponseTracker for read operation. + */ + public <E extends Endpoints<E>, P extends ReplicaPlan.ForRead<E, P>> ResponseTracker createTrackerForRead(P plan) + { + int blockFor = plan.readQuorum(); + + // Use candidates.size() for totalReplicas to allow for speculation + // (speculation can contact additional candidates beyond initial contacts) + int totalReplicas = plan.readCandidates().size(); + + return new SimpleResponseTracker(blockFor, totalReplicas); + } + + public Paxos.Participants paxosParticipants(ClusterMetadata metadata, + TableMetadata table, + Token token, + ConsistencyLevel consistencyForConsensus, + Predicate<Replica> isReplicaAlive) + { + + KeyspaceMetadata keyspaceMetadata = metadata.schema.getKeyspaceMetadata(table.keyspace); + // MetaStrategy distributes the entire keyspace to all replicas. In addition, its tables (currently only + // the dist log table) don't use the globally configured partitioner. For these reasons we don't lookup the + // replicas using the supplied token as this can actually be of the incorrect type (for example when + // performing Paxos repair). + final Token actualToken = table.partitioner == MetaStrategy.partitioner ? MetaStrategy.entireRange.right : token; + ReplicaLayout.ForTokenWrite all = forTokenWriteLiveAndDown(keyspaceMetadata, actualToken); + ReplicaLayout.ForTokenWrite electorate = consistencyForConsensus.isDatacenterLocal() + ? all.filter(InOurDc.replicas()) : all; + + EndpointsForToken live = all.all().filter(isReplicaAlive); + return new Paxos.Participants(metadata.epoch, Keyspace.open(table.keyspace), consistencyForConsensus, all, electorate, live, + (cm) -> Paxos.Participants.get(cm, table, actualToken, consistencyForConsensus)); + } + + /** + * Create ResponseTracker for write operation based on consistency level. + */ + @VisibleForTesting + public ResponseTracker createTrackerForWrite(ConsistencyLevel cl, ReplicaPlan.ForWrite plan, Endpoints<?> pending, ClusterMetadata metadata) + { + switch (cl) + { + case ANY: + case ONE: + case TWO: + case THREE: + case QUORUM: + case ALL: + int totalContacts = plan.contacts().size(); + if (pending.isEmpty()) + { + int blockFor = cl.blockFor(this); + return new SimpleResponseTracker(blockFor, totalContacts); + } + else + { + // Check if double count model applies (some CLs like ANY don't add pending) + int baseBlockFor = cl.blockFor(this); + int totalBlockFor = cl.blockForWrite(this, pending); + + // If totalBlockFor == baseBlockFor, no double-count needed (e.g., ANY) + if (totalBlockFor == baseBlockFor) + return new SimpleResponseTracker(baseBlockFor, totalContacts); + + // Double count model: committed must satisfy base CL, total must include pending + int pendingReplicas = pending.size(); + // contacts() includes both committed and pending replicas + int committedReplicas = totalContacts - pendingReplicas; + return new WriteResponseTracker(baseBlockFor, totalBlockFor, + committedReplicas, pendingReplicas, + endpoint -> pending.endpoints().contains(endpoint)); + } + + case LOCAL_ONE: + case LOCAL_QUORUM: + int localContacts = plan.contacts().filter(InOurDc.replicas()).size(); + if (pending.isEmpty()) + { + int localBlockFor = cl.blockFor(this); + return new SimpleResponseTracker(localBlockFor, localContacts, InOurDc.endpoints()); + } + else + { + // Check if double count model applies (depends on local pending) + int baseBlockFor = cl.blockFor(this); + int totalBlockFor = cl.blockForWrite(this, pending); + + // If totalBlockFor == baseBlockFor, no local pending so no double-count needed + if (totalBlockFor == baseBlockFor) + return new SimpleResponseTracker(baseBlockFor, localContacts, InOurDc.endpoints()); + + // Double count model for local DC + int localPending = pending.count(InOurDc.replicas()); + // localContacts includes both committed and pending in local DC + int localCommitted = localContacts - localPending; + return new WriteResponseTracker(baseBlockFor, totalBlockFor, + localCommitted, localPending, + endpoint -> pending.endpoints().contains(endpoint), + InOurDc.endpoints()); + } Review Comment: Can be simplified to : ``` { int localContacts = plan.contacts().filter(InOurDc.replicas()).size(); int baseBlockFor = cl.blockFor(this); int totalBlockFor = pending.isEmpty() ? baseBlockFor : cl.blockForWrite(this, pending); // Check if double count model applies (depends on local pending) // If totalBlockFor == baseBlockFor, no local pending so no double-count needed if (totalBlockFor == baseBlockFor) return new SimpleResponseTracker(baseBlockFor, localContacts, InOurDc.endpoints()); // Double count model for local DC int localPending = pending.count(InOurDc.replicas()); // localContacts includes both natural and pending in local DC int localNatural = localContacts - localPending; return new WriteResponseTracker(baseBlockFor, totalBlockFor, localNatural, localPending, endpoint -> pending.endpoints().contains(endpoint), InOurDc.endpoints()); } ``` ########## src/java/org/apache/cassandra/service/reads/tracked/TrackedRead.java: ########## @@ -285,7 +285,7 @@ private void start(Dispatcher.RequestTime requestTime, Consumer<PartialTrackedRe { // TODO: skip local coordination if this node knows its recovering from an outage // TODO: read speculation - Replica localReplica = replicaPlan.lookup(FBUtilities.getBroadcastAddressAndPort()); + Replica localReplica = plan.replicas().lookup(FBUtilities.getBroadcastAddressAndPort()); Review Comment: NIT extract `plan.replicas()` into a local variable and reuse it below: ``` P replicaPlan = plan.replicas(); Replica localReplica = replicaPlan.lookup(FBUtilities.getBroadcastAddressAndPort()); ``` ########## src/java/org/apache/cassandra/service/reads/ReplicaFilteringProtection.java: ########## @@ -636,20 +636,20 @@ private UnfilteredPartitionIterator fetchFromSource() key, filter); - ReplicaPlan.ForTokenRead replicaPlan = ReplicaPlans.forSingleReplicaRead(keyspace, key.getToken(), source); + CoordinationPlan.ForTokenRead plan = CoordinationPlan.forSingleReplicaTokenRead(keyspace, key.getToken(), source); try { - return executeReadCommand(cmd, source, ReplicaPlan.shared(replicaPlan)); + return executeReadCommand(cmd, source, plan); Review Comment: Do we no longer need to make it shared here? ########## src/java/org/apache/cassandra/service/reads/ReadCallback.java: ########## @@ -141,8 +137,8 @@ public void awaitResults() throws ReadFailureException, ReadTimeoutException * See {@link DigestResolver#preprocess(Message)} * CASSANDRA-16097 */ - int received = resolver.responses.size(); - boolean failed = failures > 0 && (replicaPlan().readQuorum() > received || !resolver.isDataPresent()); + int received = plan.responses().received(); Review Comment: NIT: assign `plan.responses()` to a local variable and use it below ``` ResponseTracker replicaPlan = plan.responses(); int received = replicaPlan.received(); ``` ########## src/java/org/apache/cassandra/service/AbstractWriteResponseHandler.java: ########## @@ -345,14 +357,17 @@ public Dispatcher.RequestTime getRequestTime() protected void signal() { + if (condition.isSignalled()) + return; + //The ideal CL should only count as a strike if the requested CL was achieved. //If the requested CL is not achieved it's fine for the ideal CL to also not be achieved. - if (idealCLDelegate != null && blockFor() + failures <= candidateReplicaCount()) + if (idealCLDelegate != null && plan.responses().isSuccessful()) Review Comment: do we also need to check for completeness here? ``` if (plan.responses().isComplete() && !plan.responses().isSuccessful()) ``` ########## src/java/org/apache/cassandra/service/paxos/Paxos.java: ########## @@ -464,26 +462,15 @@ public void collectFailure(InetAddressAndPort inetAddressAndPort, RequestFailure } - static Participants get(ClusterMetadata metadata, TableMetadata table, Token token, ConsistencyLevel consistencyForConsensus) + public static Participants get(ClusterMetadata metadata, TableMetadata table, Token token, ConsistencyLevel consistencyForConsensus) { return get(metadata, table, token, consistencyForConsensus, FailureDetector.isReplicaAlive); } static Participants get(ClusterMetadata metadata, TableMetadata table, Token token, ConsistencyLevel consistencyForConsensus, Predicate<Replica> isReplicaAlive) { - KeyspaceMetadata keyspaceMetadata = metadata.schema.getKeyspaceMetadata(table.keyspace); - // MetaStrategy distributes the entire keyspace to all replicas. In addition, its tables (currently only - // the dist log table) don't use the globally configured partitioner. For these reasons we don't lookup the - // replicas using the supplied token as this can actually be of the incorrect type (for example when - // performing Paxos repair). - final Token actualToken = table.partitioner == MetaStrategy.partitioner ? MetaStrategy.entireRange.right : token; - ReplicaLayout.ForTokenWrite all = forTokenWriteLiveAndDown(keyspaceMetadata, actualToken); - ReplicaLayout.ForTokenWrite electorate = consistencyForConsensus.isDatacenterLocal() - ? all.filter(InOurDc.replicas()) : all; - - EndpointsForToken live = all.all().filter(isReplicaAlive); - return new Participants(metadata.epoch, Keyspace.open(table.keyspace), consistencyForConsensus, all, electorate, live, - (cm) -> get(cm, table, actualToken, consistencyForConsensus)); + AbstractReplicationStrategy strategy = Keyspace.open(table.keyspace).getReplicationStrategy(); Review Comment: can the RF coming from `metadata.schema.getKeyspaceMetadata(table.keyspace)` diverge from `Keyspace.open(table.keyspace).getReplicationStrategy()` ? should we use the RF from `metadata` here instead? ########## src/java/org/apache/cassandra/service/AbstractWriteResponseHandler.java: ########## @@ -145,7 +154,7 @@ public void get() throws WriteTimeoutException, WriteFailureException, RetryOnDi throwTimeout(); int candidateReplicaCount = candidateReplicaCount(); - if (blockFor() + failures > candidateReplicaCount) + if (!plan.responses().isSuccessful()) Review Comment: do we need to check for completeness here too? ``` if (plan.responses().isComplete() && !plan.responses().isSuccessful()) ``` ########## src/java/org/apache/cassandra/locator/AbstractReplicationStrategy.java: ########## @@ -437,4 +461,376 @@ public RangesAtEndpoint getLocalRanges(ClusterMetadata cm) return newRanges; } } + + protected CoordinationPlan.ForWrite planForWriteInternal(ClusterMetadata metadata, + Keyspace keyspace, + ConsistencyLevel consistencyLevel, + Function<ClusterMetadata, ReplicaLayout.ForTokenWrite> liveAndDown, + ReplicaPlans.Selector selector) + { + ReplicaPlan.ForWrite plan = ReplicaPlans.forWrite(metadata, keyspace, consistencyLevel, liveAndDown, selector); + ResponseTracker tracker = createTrackerForWrite(consistencyLevel, plan, plan.pending, metadata); + return new CoordinationPlan.ForWrite(plan, tracker); + } + + public CoordinationPlan.ForWriteWithIdeal planForWrite(ClusterMetadata metadata, + Keyspace keyspace, + ConsistencyLevel consistencyLevel, + Function<ClusterMetadata, ReplicaLayout.ForTokenWrite> liveAndDown, + ReplicaPlans.Selector selector) + { + CoordinationPlan.ForWrite actual = planForWriteInternal(metadata, keyspace, consistencyLevel, liveAndDown, selector); + + CoordinationPlan.ForWrite ideal = null; + ConsistencyLevel idealCL = DatabaseDescriptor.getIdealConsistencyLevel(); + if (idealCL != null) + { + if (idealCL == consistencyLevel) + { + ideal = actual; + } + else + { + ideal = planForWriteInternal(metadata, keyspace, idealCL, liveAndDown, selector); + } + } + + return new CoordinationPlan.ForWriteWithIdeal(metadata, actual.replicas(), actual.responses(), ideal); + } + + public CoordinationPlan.ForWriteWithIdeal planForWrite(ClusterMetadata metadata, + Keyspace keyspace, + ConsistencyLevel consistencyLevel, + Token token, + ReplicaPlans.Selector selector) + { + return planForWrite(metadata, keyspace, consistencyLevel, + (newClusterMetadata) -> ReplicaLayout.forTokenWriteLiveAndDown(newClusterMetadata, keyspace, token), selector); + } + + /** + * Create coordination plan for forwarding a counter write to the leader replica. + * + * In cases where the original coordinator is not a replica of the counter key, the counter + * mutation is forwarded to a leader replica that will coordinate the actual counter update. + */ + public CoordinationPlan.ForWrite planForForwardingCounterWrite(ClusterMetadata metadata, + Keyspace keyspace, + Token token, + Function<ClusterMetadata, Replica> replicaSupplier) + { + ReplicaPlan.ForWrite plan = ReplicaPlans.forSingleReplicaWrite(metadata, keyspace, token, replicaSupplier); + ResponseTracker tracker = createTrackerForWrite(plan.consistencyLevel(), plan, plan.pending, metadata); + + return new CoordinationPlan.ForWriteWithIdeal(metadata, plan, tracker, null); + } + + /** + * Create coordination plan for replaying a mutation from the batchlog. + * + * When recovering failed batches, mutations are replayed to remote replicas only + * (local replica is handled separately). This method creates a replica plan + * targeting live remote replicas with CL.ONE, and a response tracker that waits on + * all contacts + */ + public CoordinationPlan.ForWriteWithIdeal planForReplayMutation(ClusterMetadata metadata, + Keyspace keyspace, + Token token) + { + Preconditions.checkState(!replicationType.isTracked(), "Batch replay not supported with tracked keyspaces"); + + ReplicaPlan.ForWrite plan = ReplicaPlans.forReplayMutation(metadata, keyspace, token); + + // wait until all contacts respond + int blockFor = plan.contacts().size(); + ResponseTracker tracker = new SimpleResponseTracker(blockFor, blockFor); + + return new CoordinationPlan.ForWriteWithIdeal(metadata, plan, tracker, null); + } + + /** + * Create coordination plan for a single-partition token read. + */ + public CoordinationPlan.ForTokenRead planForTokenRead(ClusterMetadata metadata, + Keyspace keyspace, + TableId tableId, + Token token, + @Nullable Index.QueryPlan indexQueryPlan, + ConsistencyLevel consistencyLevel, + SpeculativeRetryPolicy retry, + ReadCoordinator coordinator) + { + ReplicaPlan.ForTokenRead plan = ReplicaPlans.forRead(metadata, keyspace, tableId, token, indexQueryPlan, consistencyLevel, retry, coordinator); + ReplicaPlan.SharedForTokenRead shared = ReplicaPlan.shared(plan); + ResponseTracker tracker = createTrackerForRead(plan); + return new CoordinationPlan.ForTokenRead(shared, tracker); + } + + /** + * Create coordination plan for a range read. + */ + public CoordinationPlan.ForRangeRead planForRangeRead(ClusterMetadata metadata, + Keyspace keyspace, + TableId tableId, + @Nullable Index.QueryPlan indexQueryPlan, + ConsistencyLevel consistencyLevel, + AbstractBounds<PartitionPosition> range, + int vnodeCount) + { + ReplicaPlan.ForRangeRead plan = ReplicaPlans.forRangeRead(metadata, keyspace, tableId, indexQueryPlan, consistencyLevel, range, vnodeCount, true); + ReplicaPlan.SharedForRangeRead shared = ReplicaPlan.shared(plan); + ResponseTracker tracker = createTrackerForRead(plan); + return new CoordinationPlan.ForRangeRead(shared, tracker); + } + + /** + * Attempt to merge two adjacent range read coordination plans into one. + * + * If the two plans share enough live endpoints to satisfy the consistency level + * and the merge is worthwhile returns a merged plan otherwise returns null. + */ + public CoordinationPlan.ForRangeRead maybeMergeRangeReads(ClusterMetadata metadata, + Keyspace keyspace, + TableId tableId, + ConsistencyLevel consistencyLevel, + ReplicaPlan.ForRangeRead left, + ReplicaPlan.ForRangeRead right) + { + ReplicaPlan.ForRangeRead merged = ReplicaPlans.maybeMerge(metadata, keyspace, tableId, consistencyLevel, left, right); + if (merged == null) + return null; + + ReplicaPlan.SharedForRangeRead shared = ReplicaPlan.shared(merged); + ResponseTracker tracker = createTrackerForRead(merged); + return new CoordinationPlan.ForRangeRead(shared, tracker); + } + + /** + * Create coordination plan for a full range read + */ + public CoordinationPlan.ForRangeRead planForFullRangeRead(Keyspace keyspace, + ConsistencyLevel consistencyLevel, + AbstractBounds<PartitionPosition> range, + Set<InetAddressAndPort> endpointsToContact, + int vnodeCount) + { + ReplicaPlan.ForRangeRead plan = ReplicaPlans.forFullRangeRead(keyspace, consistencyLevel, range, endpointsToContact, vnodeCount); + ReplicaPlan.SharedForRangeRead shared = ReplicaPlan.shared(plan); + ResponseTracker tracker = createTrackerForRead(plan); + return new CoordinationPlan.ForRangeRead(shared, tracker); + } + + /** + * Create coordination plan for a single-replica token read. + */ + public CoordinationPlan.ForTokenRead planForSingleReplicaTokenRead(Keyspace keyspace, Token token, Replica replica) + { + ReplicaPlan.ForTokenRead plan = ReplicaPlans.forSingleReplicaRead(keyspace, token, replica); + ReplicaPlan.SharedForTokenRead shared = ReplicaPlan.shared(plan); + ResponseTracker tracker = createTrackerForRead(plan); + return new CoordinationPlan.ForTokenRead(shared, tracker); + } + + /** + * Create coordination plan for a single-replica range read. + * + * Used by short read protection to fetch additional partitions from a + * specific replica. blockFor=1, totalReplicas=1. + */ + public CoordinationPlan.ForRangeRead planForSingleReplicaRangeRead(Keyspace keyspace, + AbstractBounds<PartitionPosition> range, + Replica replica, + int vnodeCount) + { + ReplicaPlan.ForRangeRead plan = ReplicaPlans.forSingleReplicaRead(keyspace, range, replica, vnodeCount); + ReplicaPlan.SharedForRangeRead shared = ReplicaPlan.shared(plan); + ResponseTracker tracker = createTrackerForRead(plan); + return new CoordinationPlan.ForRangeRead(shared, tracker); + } + + /** + * Create ResponseTracker for read operation. + */ + public <E extends Endpoints<E>, P extends ReplicaPlan.ForRead<E, P>> ResponseTracker createTrackerForRead(P plan) + { + int blockFor = plan.readQuorum(); + + // Use candidates.size() for totalReplicas to allow for speculation + // (speculation can contact additional candidates beyond initial contacts) + int totalReplicas = plan.readCandidates().size(); + + return new SimpleResponseTracker(blockFor, totalReplicas); + } + + public Paxos.Participants paxosParticipants(ClusterMetadata metadata, + TableMetadata table, + Token token, + ConsistencyLevel consistencyForConsensus, + Predicate<Replica> isReplicaAlive) + { + + KeyspaceMetadata keyspaceMetadata = metadata.schema.getKeyspaceMetadata(table.keyspace); + // MetaStrategy distributes the entire keyspace to all replicas. In addition, its tables (currently only + // the dist log table) don't use the globally configured partitioner. For these reasons we don't lookup the + // replicas using the supplied token as this can actually be of the incorrect type (for example when + // performing Paxos repair). + final Token actualToken = table.partitioner == MetaStrategy.partitioner ? MetaStrategy.entireRange.right : token; + ReplicaLayout.ForTokenWrite all = forTokenWriteLiveAndDown(keyspaceMetadata, actualToken); Review Comment: `forTokenWriteLiveAndDown` uses ClusterMetadata.current(), which might diverge from the `ClusterMetadata` metadata object here. I am also wondering why we are not doing this instead: ``` ReplicaLayout.ForTokenWrite all = forTokenWriteLiveAndDown(metadata, keyspaceMetadata, actualToken); ``` ########## src/java/org/apache/cassandra/locator/AbstractReplicationStrategy.java: ########## @@ -437,4 +461,376 @@ public RangesAtEndpoint getLocalRanges(ClusterMetadata cm) return newRanges; } } + + protected CoordinationPlan.ForWrite planForWriteInternal(ClusterMetadata metadata, + Keyspace keyspace, + ConsistencyLevel consistencyLevel, + Function<ClusterMetadata, ReplicaLayout.ForTokenWrite> liveAndDown, + ReplicaPlans.Selector selector) + { + ReplicaPlan.ForWrite plan = ReplicaPlans.forWrite(metadata, keyspace, consistencyLevel, liveAndDown, selector); + ResponseTracker tracker = createTrackerForWrite(consistencyLevel, plan, plan.pending, metadata); + return new CoordinationPlan.ForWrite(plan, tracker); + } + + public CoordinationPlan.ForWriteWithIdeal planForWrite(ClusterMetadata metadata, + Keyspace keyspace, + ConsistencyLevel consistencyLevel, + Function<ClusterMetadata, ReplicaLayout.ForTokenWrite> liveAndDown, + ReplicaPlans.Selector selector) + { + CoordinationPlan.ForWrite actual = planForWriteInternal(metadata, keyspace, consistencyLevel, liveAndDown, selector); + + CoordinationPlan.ForWrite ideal = null; + ConsistencyLevel idealCL = DatabaseDescriptor.getIdealConsistencyLevel(); + if (idealCL != null) + { + if (idealCL == consistencyLevel) + { + ideal = actual; + } + else + { + ideal = planForWriteInternal(metadata, keyspace, idealCL, liveAndDown, selector); + } + } + + return new CoordinationPlan.ForWriteWithIdeal(metadata, actual.replicas(), actual.responses(), ideal); + } + + public CoordinationPlan.ForWriteWithIdeal planForWrite(ClusterMetadata metadata, + Keyspace keyspace, + ConsistencyLevel consistencyLevel, + Token token, + ReplicaPlans.Selector selector) + { + return planForWrite(metadata, keyspace, consistencyLevel, + (newClusterMetadata) -> ReplicaLayout.forTokenWriteLiveAndDown(newClusterMetadata, keyspace, token), selector); + } + + /** + * Create coordination plan for forwarding a counter write to the leader replica. + * + * In cases where the original coordinator is not a replica of the counter key, the counter + * mutation is forwarded to a leader replica that will coordinate the actual counter update. + */ + public CoordinationPlan.ForWrite planForForwardingCounterWrite(ClusterMetadata metadata, + Keyspace keyspace, + Token token, + Function<ClusterMetadata, Replica> replicaSupplier) + { + ReplicaPlan.ForWrite plan = ReplicaPlans.forSingleReplicaWrite(metadata, keyspace, token, replicaSupplier); + ResponseTracker tracker = createTrackerForWrite(plan.consistencyLevel(), plan, plan.pending, metadata); + + return new CoordinationPlan.ForWriteWithIdeal(metadata, plan, tracker, null); + } + + /** + * Create coordination plan for replaying a mutation from the batchlog. + * + * When recovering failed batches, mutations are replayed to remote replicas only + * (local replica is handled separately). This method creates a replica plan + * targeting live remote replicas with CL.ONE, and a response tracker that waits on + * all contacts + */ + public CoordinationPlan.ForWriteWithIdeal planForReplayMutation(ClusterMetadata metadata, + Keyspace keyspace, + Token token) + { + Preconditions.checkState(!replicationType.isTracked(), "Batch replay not supported with tracked keyspaces"); + + ReplicaPlan.ForWrite plan = ReplicaPlans.forReplayMutation(metadata, keyspace, token); + + // wait until all contacts respond + int blockFor = plan.contacts().size(); + ResponseTracker tracker = new SimpleResponseTracker(blockFor, blockFor); + + return new CoordinationPlan.ForWriteWithIdeal(metadata, plan, tracker, null); + } + + /** + * Create coordination plan for a single-partition token read. + */ + public CoordinationPlan.ForTokenRead planForTokenRead(ClusterMetadata metadata, + Keyspace keyspace, + TableId tableId, + Token token, + @Nullable Index.QueryPlan indexQueryPlan, + ConsistencyLevel consistencyLevel, + SpeculativeRetryPolicy retry, + ReadCoordinator coordinator) + { + ReplicaPlan.ForTokenRead plan = ReplicaPlans.forRead(metadata, keyspace, tableId, token, indexQueryPlan, consistencyLevel, retry, coordinator); + ReplicaPlan.SharedForTokenRead shared = ReplicaPlan.shared(plan); + ResponseTracker tracker = createTrackerForRead(plan); + return new CoordinationPlan.ForTokenRead(shared, tracker); + } + + /** + * Create coordination plan for a range read. + */ + public CoordinationPlan.ForRangeRead planForRangeRead(ClusterMetadata metadata, + Keyspace keyspace, + TableId tableId, + @Nullable Index.QueryPlan indexQueryPlan, + ConsistencyLevel consistencyLevel, + AbstractBounds<PartitionPosition> range, + int vnodeCount) + { + ReplicaPlan.ForRangeRead plan = ReplicaPlans.forRangeRead(metadata, keyspace, tableId, indexQueryPlan, consistencyLevel, range, vnodeCount, true); + ReplicaPlan.SharedForRangeRead shared = ReplicaPlan.shared(plan); + ResponseTracker tracker = createTrackerForRead(plan); + return new CoordinationPlan.ForRangeRead(shared, tracker); + } + + /** + * Attempt to merge two adjacent range read coordination plans into one. + * + * If the two plans share enough live endpoints to satisfy the consistency level + * and the merge is worthwhile returns a merged plan otherwise returns null. + */ + public CoordinationPlan.ForRangeRead maybeMergeRangeReads(ClusterMetadata metadata, + Keyspace keyspace, + TableId tableId, + ConsistencyLevel consistencyLevel, + ReplicaPlan.ForRangeRead left, + ReplicaPlan.ForRangeRead right) + { + ReplicaPlan.ForRangeRead merged = ReplicaPlans.maybeMerge(metadata, keyspace, tableId, consistencyLevel, left, right); + if (merged == null) + return null; + + ReplicaPlan.SharedForRangeRead shared = ReplicaPlan.shared(merged); + ResponseTracker tracker = createTrackerForRead(merged); + return new CoordinationPlan.ForRangeRead(shared, tracker); + } + + /** + * Create coordination plan for a full range read + */ + public CoordinationPlan.ForRangeRead planForFullRangeRead(Keyspace keyspace, + ConsistencyLevel consistencyLevel, + AbstractBounds<PartitionPosition> range, + Set<InetAddressAndPort> endpointsToContact, + int vnodeCount) + { + ReplicaPlan.ForRangeRead plan = ReplicaPlans.forFullRangeRead(keyspace, consistencyLevel, range, endpointsToContact, vnodeCount); + ReplicaPlan.SharedForRangeRead shared = ReplicaPlan.shared(plan); + ResponseTracker tracker = createTrackerForRead(plan); + return new CoordinationPlan.ForRangeRead(shared, tracker); + } + + /** + * Create coordination plan for a single-replica token read. + */ + public CoordinationPlan.ForTokenRead planForSingleReplicaTokenRead(Keyspace keyspace, Token token, Replica replica) + { + ReplicaPlan.ForTokenRead plan = ReplicaPlans.forSingleReplicaRead(keyspace, token, replica); + ReplicaPlan.SharedForTokenRead shared = ReplicaPlan.shared(plan); + ResponseTracker tracker = createTrackerForRead(plan); + return new CoordinationPlan.ForTokenRead(shared, tracker); + } + + /** + * Create coordination plan for a single-replica range read. + * + * Used by short read protection to fetch additional partitions from a + * specific replica. blockFor=1, totalReplicas=1. + */ + public CoordinationPlan.ForRangeRead planForSingleReplicaRangeRead(Keyspace keyspace, + AbstractBounds<PartitionPosition> range, + Replica replica, + int vnodeCount) + { + ReplicaPlan.ForRangeRead plan = ReplicaPlans.forSingleReplicaRead(keyspace, range, replica, vnodeCount); + ReplicaPlan.SharedForRangeRead shared = ReplicaPlan.shared(plan); + ResponseTracker tracker = createTrackerForRead(plan); + return new CoordinationPlan.ForRangeRead(shared, tracker); + } + + /** + * Create ResponseTracker for read operation. + */ + public <E extends Endpoints<E>, P extends ReplicaPlan.ForRead<E, P>> ResponseTracker createTrackerForRead(P plan) + { + int blockFor = plan.readQuorum(); + + // Use candidates.size() for totalReplicas to allow for speculation + // (speculation can contact additional candidates beyond initial contacts) + int totalReplicas = plan.readCandidates().size(); + + return new SimpleResponseTracker(blockFor, totalReplicas); + } + + public Paxos.Participants paxosParticipants(ClusterMetadata metadata, + TableMetadata table, + Token token, + ConsistencyLevel consistencyForConsensus, + Predicate<Replica> isReplicaAlive) + { + + KeyspaceMetadata keyspaceMetadata = metadata.schema.getKeyspaceMetadata(table.keyspace); + // MetaStrategy distributes the entire keyspace to all replicas. In addition, its tables (currently only + // the dist log table) don't use the globally configured partitioner. For these reasons we don't lookup the + // replicas using the supplied token as this can actually be of the incorrect type (for example when + // performing Paxos repair). + final Token actualToken = table.partitioner == MetaStrategy.partitioner ? MetaStrategy.entireRange.right : token; + ReplicaLayout.ForTokenWrite all = forTokenWriteLiveAndDown(keyspaceMetadata, actualToken); + ReplicaLayout.ForTokenWrite electorate = consistencyForConsensus.isDatacenterLocal() + ? all.filter(InOurDc.replicas()) : all; + + EndpointsForToken live = all.all().filter(isReplicaAlive); + return new Paxos.Participants(metadata.epoch, Keyspace.open(table.keyspace), consistencyForConsensus, all, electorate, live, + (cm) -> Paxos.Participants.get(cm, table, actualToken, consistencyForConsensus)); + } + + /** + * Hook for replication strategies to send additional mutations alongside a paxos commit. + * Called from PaxosCommit.start() after local synchronous execution for tracked keyspaces. + * + * If the method doesn't return null, the returned future is composed with the paxos consensus: + * onDone fires only after both the paxos quorum decision AND this future complete, or after + * one of them fails. + */ + public Future<Void> sendPaxosCommitMutations(Agreed commit, boolean isUrgent) + { + return null; + } + + /** + * Check whether paxos operations should be rejected for the given token. + */ + public boolean shouldRejectPaxos(Token token) + { + return false; + } + + /** + * Create ResponseTracker for write operation based on consistency level. + */ + @VisibleForTesting + public ResponseTracker createTrackerForWrite(ConsistencyLevel cl, ReplicaPlan.ForWrite plan, Endpoints<?> pending, ClusterMetadata metadata) + { + switch (cl) + { + case ANY: + case ONE: + case TWO: + case THREE: + case QUORUM: + case ALL: + int totalContacts = plan.contacts().size(); + if (pending.isEmpty()) + { + int blockFor = cl.blockFor(this); + return new SimpleResponseTracker(blockFor, totalContacts); + } + else + { + // Check if double count model applies (some CLs like ANY don't add pending) + int baseBlockFor = cl.blockFor(this); + int totalBlockFor = cl.blockForWrite(this, pending); + + // If totalBlockFor == baseBlockFor, no double-count needed (e.g., ANY) + if (totalBlockFor == baseBlockFor) + return new SimpleResponseTracker(baseBlockFor, totalContacts); + + // Double count model: committed must satisfy base CL, total must include pending + int pendingReplicas = pending.size(); + // contacts() includes both committed and pending replicas + int committedReplicas = totalContacts - pendingReplicas; + return new WriteResponseTracker(baseBlockFor, totalBlockFor, + committedReplicas, pendingReplicas, + endpoint -> pending.endpoints().contains(endpoint)); + } + + case LOCAL_ONE: + case LOCAL_QUORUM: + int localContacts = plan.contacts().filter(InOurDc.replicas()).size(); + if (pending.isEmpty()) + { + int localBlockFor = cl.blockFor(this); + return new SimpleResponseTracker(localBlockFor, localContacts, InOurDc.endpoints()); + } + else + { + // Check if double count model applies (depends on local pending) + int baseBlockFor = cl.blockFor(this); + int totalBlockFor = cl.blockForWrite(this, pending); + + // If totalBlockFor == baseBlockFor, no local pending so no double-count needed + if (totalBlockFor == baseBlockFor) + return new SimpleResponseTracker(baseBlockFor, localContacts, InOurDc.endpoints()); + + // Double count model for local DC + int localPending = pending.count(InOurDc.replicas()); + // localContacts includes both committed and pending in local DC + int localCommitted = localContacts - localPending; + return new WriteResponseTracker(baseBlockFor, totalBlockFor, + localCommitted, localPending, + endpoint -> pending.endpoints().contains(endpoint), + InOurDc.endpoints()); + } + + case EACH_QUORUM: + return createPerDcTracker(plan, pending, metadata); + + default: + throw new UnsupportedOperationException("Unsupported consistency level for writes: " + cl); + } + } + + /** + * Create per-datacenter tracker for EACH_QUORUM. + */ + private ResponseTracker createPerDcTracker(ReplicaPlan.ForWrite plan, Endpoints<?> pending, ClusterMetadata metadata) + { + Map<String, ResponseTracker> trackerPerDc = new HashMap<>(); + Locator locator = metadata.locator; + + // Group replicas by datacenter + Map<String, List<Replica>> replicasByDc = new HashMap<>(); + for (Replica replica : plan.contacts()) + { + String dc = locator.location(replica.endpoint()).datacenter; + replicasByDc.computeIfAbsent(dc, k -> new ArrayList<>()).add(replica); + } + + // Group pending replicas by datacenter + Map<String, List<Replica>> pendingByDc = new HashMap<>(); + for (Replica replica : pending) + { + String dc = locator.location(replica.endpoint()).datacenter; + pendingByDc.computeIfAbsent(dc, k -> new ArrayList<>()).add(replica); + } + + // Create tracker for each DC + for (Map.Entry<String, List<Replica>> entry : replicasByDc.entrySet()) + { + String dc = entry.getKey(); + int dcContacts = entry.getValue().size(); + List<Replica> dcPending = pendingByDc.getOrDefault(dc, Collections.emptyList()); + int dcPendingCount = dcPending.size(); + int dcCommitted = dcContacts - dcPendingCount; + int dcBlockFor = dcCommitted / 2 + 1; Review Comment: I had the same question, but I think it would be handled with `int totalBlockFor = dcBlockFor + dcPendingCount;` ########## src/java/org/apache/cassandra/locator/WriteResponseTracker.java: ########## @@ -0,0 +1,237 @@ +/* + * 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.locator; + +import java.util.function.Predicate; + +import org.apache.cassandra.exceptions.RequestFailureReason; + +/** + * Response tracker for writes with pending replicas using the double count model. + * <p> + * Two requirements must be satisfied for success: + * <ol> + * <li>Committed replicas must satisfy base CL: {@code committedSuccesses >= baseBlockFor}</li> + * <li>Total replicas (committed + pending) must satisfy: {@code totalSuccesses >= totalBlockFor}</li> + * </ol> + * <p> + * This ensures both consistency (committed replicas have the data) and bootstrap safety + * (pending replicas also receive the write). + * <p> + * Implemented by composing two {@link SimpleResponseTracker}s: + * <ul> + * <li>committedTracker: tracks responses from committed replicas only</li> + * <li>totalTracker: tracks responses from all replicas (committed + pending)</li> + * </ul> + * <p> + * Thread-safe through delegation to thread-safe SimpleResponseTrackers. + */ +public class WriteResponseTracker implements ResponseTracker +{ + private final SimpleResponseTracker natural; + private final SimpleResponseTracker total; + + /** + * Create a write response tracker with the double count model. + * + * @param baseBlockFor number of committed replica responses required for CL + * @param totalBlockFor total responses required (baseBlockFor + pending count) + * @param committedReplicas number of committed replicas available + * @param pendingReplicas number of pending replicas available + * @param isPending predicate to determine if an endpoint is pending + */ + public WriteResponseTracker(int baseBlockFor, + int totalBlockFor, + int committedReplicas, + int pendingReplicas, + Predicate<InetAddressAndPort> isPending) + { + this(baseBlockFor, totalBlockFor, committedReplicas, pendingReplicas, isPending, null); + } + + /** + * Create a write response tracker with the double count model and a filter. + * + * @param naturalBlockFor number of committed replica responses required for CL + * @param totalBlockFor total responses required (naturalBlockFor + pending count) + * @param naturalReplicas number of committed replicas available + * @param pendingReplicas number of pending replicas available + * @param isPending predicate to determine if an endpoint is pending + * @param filter predicate to filter which responses count (e.g., InOurDc for LOCAL_QUORUM) + */ + public WriteResponseTracker(int naturalBlockFor, + int totalBlockFor, + int naturalReplicas, + int pendingReplicas, + Predicate<InetAddressAndPort> isPending, + Predicate<InetAddressAndPort> filter) + { + if (naturalBlockFor < 0) + throw new IllegalArgumentException("naturalBlockFor must be non-negative: " + naturalBlockFor); + if (totalBlockFor < naturalBlockFor) + throw new IllegalArgumentException("totalBlockFor (" + totalBlockFor + ") must be >= naturalBlockFor (" + naturalBlockFor + ")"); + if (naturalReplicas < 0) + throw new IllegalArgumentException("naturalReplicas must be non-negative: " + naturalReplicas); + if (pendingReplicas < 0) + throw new IllegalArgumentException("pendingReplicas must be non-negative: " + pendingReplicas); + if (naturalBlockFor > naturalReplicas) + throw new IllegalArgumentException("naturalBlockFor (" + naturalBlockFor + ") cannot exceed naturalReplicas (" + naturalReplicas + ")"); + if (totalBlockFor > naturalReplicas + pendingReplicas) + throw new IllegalArgumentException("totalBlockFor (" + totalBlockFor + ") cannot exceed total replicas (" + (naturalReplicas + pendingReplicas) + ")"); + if (isPending == null) + throw new IllegalArgumentException("isPending predicate cannot be null"); + + Predicate<InetAddressAndPort> naturalFilter = isPending.negate(); + if (filter != null) + naturalFilter = naturalFilter.and(filter); + + this.natural = new SimpleResponseTracker(naturalBlockFor, naturalReplicas, naturalFilter); + this.total = new SimpleResponseTracker(totalBlockFor, naturalReplicas + pendingReplicas, filter); + } + + private WriteResponseTracker(SimpleResponseTracker natural, SimpleResponseTracker total) + { + this.natural = natural; + this.total = total; + } + + @Override + public void onResponse(InetAddressAndPort from) + { + natural.onResponse(from); + total.onResponse(from); + } + + @Override + public void onFailure(InetAddressAndPort from, RequestFailureReason reason) + { + natural.onFailure(from, reason); + total.onFailure(from, reason); + } + + @Override + public boolean isComplete() + { + // Early failure if either tracker fails + if (natural.isComplete() && !natural.isSuccessful()) + return true; + if (total.isComplete() && !total.isSuccessful()) + return true; + + // Success requires both to succeed + return natural.isSuccessful() && total.isSuccessful(); + } + + @Override + public boolean isSuccessful() + { + return natural.isSuccessful() && total.isSuccessful(); + } + + @Override + public int required() + { + // Return total requirement for error messages + return total.required(); + } + + @Override + public int received() + { + // Return total successes for error messages + return total.received(); + } + + @Override + public int failures() + { + // Return total failures for error messages + return total.failures(); + } + + @Override + public boolean countsTowardQuorum(InetAddressAndPort from) + { + return total.countsTowardQuorum(from); + } + + public int committedReceived() + { + return natural.received(); + } + + public int pendingReceived() + { + return total.received() - natural.received(); + } + + public int committedFailures() + { + return natural.failures(); + } + + public int pendingFailures() Review Comment: also not atomic ########## src/java/org/apache/cassandra/service/DatacenterSyncWriteResponseHandler.java: ########## @@ -17,109 +17,69 @@ */ package org.apache.cassandra.service; -import java.util.HashMap; import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.MessageParams; import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.WriteType; +import org.apache.cassandra.locator.CoordinationPlan; +import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.Locator; -import org.apache.cassandra.locator.NetworkTopologyStrategy; -import org.apache.cassandra.locator.Replica; -import org.apache.cassandra.locator.ReplicaPlan; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.ParamType; import org.apache.cassandra.service.writes.thresholds.WriteWarningContext; import org.apache.cassandra.transport.Dispatcher; +import org.apache.cassandra.utils.FBUtilities; /** * This class blocks for a quorum of responses _in all datacenters_ (CL.EACH_QUORUM). + * + * The PerDcResponseTracker handles per-datacenter counting. */ public class DatacenterSyncWriteResponseHandler<T> extends AbstractWriteResponseHandler<T> { private static final Locator locator = DatabaseDescriptor.getLocator(); - private final Map<String, AtomicInteger> responses = new HashMap<String, AtomicInteger>(); - private final AtomicInteger acks = new AtomicInteger(0); - - public DatacenterSyncWriteResponseHandler(ReplicaPlan.ForWrite replicaPlan, + public DatacenterSyncWriteResponseHandler(CoordinationPlan.ForWrite coordinationPlan, Review Comment: `locator` is no longer used, we can remove it as well -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]

