This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch cassandra-6.0
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/cassandra-6.0 by this push:
     new 45afe53827 Block decommission if node is a participant in a CMS 
reconfiguration
45afe53827 is described below

commit 45afe538274ae70a52a3c915d55e7c8f700ff383
Author: nivy <[email protected]>
AuthorDate: Thu Sep 17 18:12:35 2026 -0400

    Block decommission if node is a participant in a CMS reconfiguration
    
    Patch by Nivy Kani; reviewed by Sam Tunnicliffe and Caleb Rackliffe for
    CASSANDRA-21591
---
 CHANGES.txt                                        |  1 +
 .../org/apache/cassandra/tcm/ClusterMetadata.java  |  3 +
 .../cassandra/tcm/sequences/ReconfigureCMS.java    |  6 +-
 .../tcm/transformations/PrepareLeave.java          |  6 ++
 .../ConcurrentCMSReconfigurationAndMoveTest.java   | 66 ++++++++++++++++
 ...CMSReconfigurationAndNodeOperationTestBase.java | 91 ++++++++++++++++++++++
 ...igurationAndReplacementOfJoiningMemberTest.java | 71 +++++++++++++++++
 ...igurationAndReplacementOfLeavingMemberTest.java | 83 ++++++++++++++++++++
 .../distributed/test/log/TestProcessor.java        | 24 +++++-
 .../tcm/transformations/PrepareLeaveTest.java      | 56 +++++++++++++
 10 files changed, 403 insertions(+), 4 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 015680a337..4413bd5fc9 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 6.0-alpha3
+ * Block decommission if node is a participant in a CMS reconfiguration 
(CASSANDRA-21591)
  * Sharding support for non-vector SAI in-memory indexes (CASSANDRA-18216)
  * Index hints serialization rejects a count that the validation accepts 
(CASSANDRA-21700)
  * Do not expose system_metrics virtual keyspace by default (CASSANDRA-21720)
diff --git a/src/java/org/apache/cassandra/tcm/ClusterMetadata.java 
b/src/java/org/apache/cassandra/tcm/ClusterMetadata.java
index 1120269c27..16261f9326 100644
--- a/src/java/org/apache/cassandra/tcm/ClusterMetadata.java
+++ b/src/java/org/apache/cassandra/tcm/ClusterMetadata.java
@@ -595,6 +595,9 @@ public class ClusterMetadata
                                                             
MultiStepOperation<?> operation,
                                                             Directory 
directory)
     {
+        if (operation.kind() == MultiStepOperation.Kind.RECONFIGURE_CMS)
+            return false;
+
         return operation.affectedPeers(directory).contains(peer);
     }
 
diff --git a/src/java/org/apache/cassandra/tcm/sequences/ReconfigureCMS.java 
b/src/java/org/apache/cassandra/tcm/sequences/ReconfigureCMS.java
index 267763404c..1055e1ecd6 100644
--- a/src/java/org/apache/cassandra/tcm/sequences/ReconfigureCMS.java
+++ b/src/java/org/apache/cassandra/tcm/sequences/ReconfigureCMS.java
@@ -153,7 +153,11 @@ public class ReconfigureCMS extends 
MultiStepOperation<AdvanceCMSReconfiguration
     @Override
     public Set<NodeId> affectedPeers(Directory directory)
     {
-        return Set.of();
+        Set<NodeId> affected = new HashSet<>(next.diff.additions);
+        affected.addAll(next.diff.removals);
+        if (next.activeTransition != null)
+            affected.add(next.activeTransition.nodeId);
+        return affected;
     }
 
     @Override
diff --git 
a/src/java/org/apache/cassandra/tcm/transformations/PrepareLeave.java 
b/src/java/org/apache/cassandra/tcm/transformations/PrepareLeave.java
index eb0e9c4a55..870f446794 100644
--- a/src/java/org/apache/cassandra/tcm/transformations/PrepareLeave.java
+++ b/src/java/org/apache/cassandra/tcm/transformations/PrepareLeave.java
@@ -32,6 +32,7 @@ import org.apache.cassandra.locator.NetworkTopologyStrategy;
 import org.apache.cassandra.schema.KeyspaceMetadata;
 import org.apache.cassandra.tcm.ClusterMetadata;
 import org.apache.cassandra.tcm.ClusterMetadataService;
+import org.apache.cassandra.tcm.MultiStepOperation;
 import org.apache.cassandra.tcm.Transformation;
 import org.apache.cassandra.tcm.membership.Directory;
 import org.apache.cassandra.tcm.membership.NodeId;
@@ -41,6 +42,7 @@ import org.apache.cassandra.tcm.ownership.PlacementProvider;
 import org.apache.cassandra.tcm.ownership.PlacementTransitionPlan;
 import org.apache.cassandra.tcm.sequences.LeaveStreams;
 import org.apache.cassandra.tcm.sequences.LockedRanges;
+import org.apache.cassandra.tcm.sequences.ReconfigureCMS;
 import org.apache.cassandra.tcm.sequences.UnbootstrapAndLeave;
 import org.apache.cassandra.tcm.serialization.AsymmetricMetadataSerializer;
 import org.apache.cassandra.tcm.serialization.Version;
@@ -92,6 +94,10 @@ public class PrepareLeave implements Transformation
         if (prev.directory.peerState(leaving) != NodeState.JOINED)
             return new Rejected(INVALID, String.format("Rejecting this plan as 
the node %s is in state %s", leaving, prev.directory.peerState(leaving)));
 
+        MultiStepOperation<?> cmsReconfiguration = 
prev.inProgressSequences.get(ReconfigureCMS.SequenceKey.instance);
+        if (cmsReconfiguration != null && 
cmsReconfiguration.affectedPeers(prev.directory).contains(leaving))
+            return new Rejected(INVALID, String.format("Rejecting this plan as 
the node %s is part of an in-progress CMS reconfiguration: %s", leaving, 
cmsReconfiguration));
+
         ClusterMetadata proposed = 
prev.transformer().proposeRemoveNode(leaving).build().metadata;
 
         if (!force && !validateReplicationForDecommission(proposed))
diff --git 
a/test/distributed/org/apache/cassandra/distributed/test/log/ConcurrentCMSReconfigurationAndMoveTest.java
 
b/test/distributed/org/apache/cassandra/distributed/test/log/ConcurrentCMSReconfigurationAndMoveTest.java
new file mode 100644
index 0000000000..fa41ddf712
--- /dev/null
+++ 
b/test/distributed/org/apache/cassandra/distributed/test/log/ConcurrentCMSReconfigurationAndMoveTest.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.distributed.test.log;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import com.google.common.collect.Iterables;
+
+import org.junit.Test;
+
+import org.apache.cassandra.distributed.Cluster;
+import org.apache.cassandra.distributed.api.IInvokableInstance;
+import org.apache.cassandra.distributed.api.NodeToolResult;
+import org.apache.cassandra.distributed.shared.ClusterUtils;
+import org.apache.cassandra.service.StorageService;
+
+public class ConcurrentCMSReconfigurationAndMoveTest extends 
ConcurrentCMSReconfigurationAndNodeOperationTestBase
+{
+    @Test
+    public void test() throws Exception
+    {
+        try (Cluster cluster = newCluster())
+        {
+            ExecutorService executor = Executors.newSingleThreadExecutor();
+            IInvokableInstance firstCMS = cluster.get(1);
+            initialCMSSetup(cluster, firstCMS, 2);
+
+            // nodes 1 & 2 are now CMS members, prepare another 
reconfiguration which will add node 3
+            // but force it to halt before the first step is committed by 
pausing commits on the node
+            // driving it (node1)
+            Future<NodeToolResult> reconfigureResult = 
beginAndPauseReconfiguration(executor, firstCMS, 3, "ADDITIONS: [/127.0.0.3");
+
+            // Now the reconfiguration which is active in cluster metadata 
includes node3, try moving it to a new token.
+            // node1 is still paused so cannot receive commit requests itself, 
but it can still participate in consensus
+            // decisions with node2
+            IInvokableInstance nodeToMove = cluster.get(3);
+            String token = nodeToMove.callsOnInstance(() -> 
Iterables.getOnlyElement(StorageService.instance.getLocalTokens()).toString()).call();
+            long moveTo = Long.parseLong(token) + 1;
+            nodeToMove.nodetoolResult("move", 
Long.toString(moveTo)).asserts().success();
+            ClusterUtils.waitForCMSToQuiesce(cluster, nodeToMove);
+
+            // now that node3 has completed its token move, unpause node1 to 
allow it to continue with the inflight
+            // CMS reconfiguration, which will succeed.
+            ClusterUtils.unpauseCommits(firstCMS);
+            reconfigureResult.get().asserts().success();
+        }
+    }
+}
diff --git 
a/test/distributed/org/apache/cassandra/distributed/test/log/ConcurrentCMSReconfigurationAndNodeOperationTestBase.java
 
b/test/distributed/org/apache/cassandra/distributed/test/log/ConcurrentCMSReconfigurationAndNodeOperationTestBase.java
new file mode 100644
index 0000000000..29fa5992d8
--- /dev/null
+++ 
b/test/distributed/org/apache/cassandra/distributed/test/log/ConcurrentCMSReconfigurationAndNodeOperationTestBase.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.distributed.test.log;
+
+import java.io.IOException;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+import org.apache.cassandra.distributed.Cluster;
+import org.apache.cassandra.distributed.api.ConsistencyLevel;
+import org.apache.cassandra.distributed.api.Feature;
+import org.apache.cassandra.distributed.api.IInvokableInstance;
+import org.apache.cassandra.distributed.api.NodeToolResult;
+import org.apache.cassandra.distributed.api.TokenSupplier;
+import org.apache.cassandra.distributed.shared.ClusterUtils;
+import org.apache.cassandra.distributed.test.TestBaseImpl;
+import org.apache.cassandra.tcm.Epoch;
+
+import static 
org.apache.cassandra.tcm.Transformation.Kind.ADVANCE_CMS_RECONFIGURATION;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Common scaffolding for tests which pause an in-flight CMS reconfiguration 
mid-commit and then
+ * perform some other node operation (replacement, move, ...) concurrently, 
before unpausing and
+ * observing the outcome of the reconfiguration.
+ */
+public abstract class ConcurrentCMSReconfigurationAndNodeOperationTestBase 
extends TestBaseImpl
+{
+    protected Cluster newCluster() throws IOException
+    {
+        // Configure a short cms_await_timeout as one of the CMS nodes will be 
paused, meaning
+        // any commit requests sent to it will eventually timeout and be 
resubmitted to another
+        // member. Also minimize the number of acks required by 
ProgressBarrier, as the paused
+        // CMS member may not respond to watermark requests if its 
INTERNAL_METADATA stage is
+        // filled by backed up commit requests.
+        TokenSupplier even = TokenSupplier.evenlyDistributedTokens(3);
+        return init(builder().withNodes(3)
+                             .withTokenSupplier(node -> even.token(node == 4 ? 
3 : node))
+                             .withoutVNodes()
+                             .withConfig(c -> c.with(Feature.GOSSIP, 
Feature.NETWORK)
+                                               .set("cms_await_timeout", "1s")
+                                               
.set("progress_barrier_default_consistency_level", ConsistencyLevel.ONE))
+                             .start());
+    }
+
+    /**
+     * Set up the CMS ready to begin the test
+     */
+    protected void initialCMSSetup(Cluster cluster, IInvokableInstance driver, 
int newSize)
+    {
+        ClusterUtils.waitForCMSToQuiesce(cluster, driver);
+        driver.nodetoolResult("cms", "reconfigure", 
Integer.toString(newSize)).asserts().success();
+        ClusterUtils.waitForCMSToQuiesce(cluster, driver);
+    }
+
+    /**
+     * Prepares another reconfiguration and forces it to halt before its first 
step is committed,
+     * by pausing commits on the node driving it, so the caller can perform a 
node operation while
+     * the reconfiguration is stalled mid-flight.
+     */
+    protected Future<NodeToolResult> 
beginAndPauseReconfiguration(ExecutorService executor,
+                                                                  
IInvokableInstance driver,
+                                                                  int newSize,
+                                                                  String 
expectedStatus) throws Exception
+    {
+        Callable<Epoch> pending = ClusterUtils.pauseBeforeCommit(driver, e -> 
e.kind() == ADVANCE_CMS_RECONFIGURATION);
+        Future<NodeToolResult> inFlightReconfig = executor.submit(() -> 
driver.nodetoolResult("cms", "reconfigure", Integer.toString(newSize)));
+        pending.call();
+
+        String status = driver.nodetoolResult("cms", "reconfigure", 
"--status").getStdout();
+        assertTrue(status.contains(expectedStatus));
+        return inFlightReconfig;
+    }
+}
diff --git 
a/test/distributed/org/apache/cassandra/distributed/test/log/ConcurrentCMSReconfigurationAndReplacementOfJoiningMemberTest.java
 
b/test/distributed/org/apache/cassandra/distributed/test/log/ConcurrentCMSReconfigurationAndReplacementOfJoiningMemberTest.java
new file mode 100644
index 0000000000..7d693943f0
--- /dev/null
+++ 
b/test/distributed/org/apache/cassandra/distributed/test/log/ConcurrentCMSReconfigurationAndReplacementOfJoiningMemberTest.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.distributed.test.log;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import org.junit.Test;
+
+import org.apache.cassandra.distributed.Cluster;
+import org.apache.cassandra.distributed.api.IInvokableInstance;
+import org.apache.cassandra.distributed.api.NodeToolResult;
+import org.apache.cassandra.distributed.shared.ClusterUtils;
+
+import static org.apache.cassandra.distributed.shared.ClusterUtils.addInstance;
+import static 
org.apache.cassandra.distributed.shared.ClusterUtils.awaitRingJoin;
+import static 
org.apache.cassandra.distributed.shared.ClusterUtils.startHostReplacement;
+
+public class ConcurrentCMSReconfigurationAndReplacementOfJoiningMemberTest 
extends ConcurrentCMSReconfigurationAndNodeOperationTestBase
+{
+    @Test
+    public void test() throws Exception
+    {
+        try (Cluster cluster = newCluster())
+        {
+            ExecutorService executor = Executors.newSingleThreadExecutor();
+            IInvokableInstance firstCMS = cluster.get(1);
+            initialCMSSetup(cluster, firstCMS, 2);
+
+            // nodes 1 & 2 are now CMS members, prepare another 
reconfiguration which will add node 3
+            // but force it to halt before the first step is committed by 
pausing commits on the node
+            // driving it (node1)
+            Future<NodeToolResult> inFlightReconfig = 
beginAndPauseReconfiguration(executor, firstCMS, 3, "ADDITIONS: [/127.0.0.3");
+
+            // Now the reconfiguration which is active in cluster metadata 
includes node3, try replacing it.
+            // node1 is still paused so cannot receive commit requests itself, 
but it can still participate
+            // in consensus decisions with node2
+            IInvokableInstance nodeToRemove = cluster.get(3);
+            nodeToRemove.shutdown().get();
+            IInvokableInstance replacingNode = addInstance(cluster, 
nodeToRemove.config(), c -> c.set("auto_bootstrap", true));
+            startHostReplacement(nodeToRemove, replacingNode, (ignore1_, 
ignore2_) -> {});
+            awaitRingJoin(cluster.get(1), replacingNode);
+            awaitRingJoin(replacingNode, cluster.get(1));
+
+            // now that node4 has replaced node3, unpause node1 to allow it to 
continue with the inflight CMS
+            // reconfiguration, which will fail
+            ClusterUtils.unpauseCommits(firstCMS);
+            inFlightReconfig.get().asserts().failure();
+            // Cancel the inflight reconfiguration and resubmit another, which 
will now succeed
+            firstCMS.nodetoolResult("cms", "reconfigure", 
"--cancel").asserts().success();
+            firstCMS.nodetoolResult("cms", "reconfigure", 
"3").asserts().success();
+        }
+    }
+}
diff --git 
a/test/distributed/org/apache/cassandra/distributed/test/log/ConcurrentCMSReconfigurationAndReplacementOfLeavingMemberTest.java
 
b/test/distributed/org/apache/cassandra/distributed/test/log/ConcurrentCMSReconfigurationAndReplacementOfLeavingMemberTest.java
new file mode 100644
index 0000000000..a11b308561
--- /dev/null
+++ 
b/test/distributed/org/apache/cassandra/distributed/test/log/ConcurrentCMSReconfigurationAndReplacementOfLeavingMemberTest.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.distributed.test.log;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import org.junit.Test;
+
+import org.apache.cassandra.distributed.Cluster;
+import org.apache.cassandra.distributed.Constants;
+import org.apache.cassandra.distributed.api.IInvokableInstance;
+import org.apache.cassandra.distributed.api.NodeToolResult;
+import org.apache.cassandra.distributed.shared.ClusterUtils;
+
+import static org.apache.cassandra.distributed.shared.ClusterUtils.addInstance;
+import static 
org.apache.cassandra.distributed.shared.ClusterUtils.startHostReplacement;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class ConcurrentCMSReconfigurationAndReplacementOfLeavingMemberTest 
extends ConcurrentCMSReconfigurationAndNodeOperationTestBase
+{
+    @Test
+    public void test() throws Exception
+    {
+        try (Cluster cluster = newCluster())
+        {
+            ExecutorService executor = Executors.newSingleThreadExecutor();
+            IInvokableInstance firstCMS = cluster.get(1);
+            initialCMSSetup(cluster, firstCMS, 3);
+
+            // nodes 1, 2 & 3 are now CMS members, prepare another 
reconfiguration which will remove node 3
+            // but force it to halt before the first step is committed by 
pausing commits on the node
+            // driving it (node1)
+            Future<NodeToolResult> inFlightReconfig = 
beginAndPauseReconfiguration(executor, firstCMS, 2, "REMOVALS: [/127.0.0.3");
+
+            // Now the reconfiguration which is active in cluster metadata 
includes node3, try replacing it.
+            // node1 is still paused so cannot receive commit requests itself, 
but it can still participate
+            // in consensus decisions with node2
+            IInvokableInstance nodeToRemove = cluster.get(3);
+            nodeToRemove.shutdown().get();
+
+            // The startup of the replacement node fails *after* 
messaging/gossip threads have started, so we configure it
+            // *not* to mark itself as shutdown on failure because this causes 
it to be skipped by the cluster shutdown
+            // at the end of the test. If not properly terminated, the 
instance remains technically alive and able to
+            // communicate with instances started by subsequent tests and 
interfere with the startup of those other clusters.
+            IInvokableInstance replacingNode = addInstance(cluster, 
nodeToRemove.config(),
+                                                           c -> 
c.set("auto_bootstrap", true)
+                                                                 
.set(Constants.KEY_DTEST_API_STARTUP_FAILURE_AS_SHUTDOWN, false));
+            try
+            {
+                startHostReplacement(nodeToRemove, replacingNode, (ignore1_, 
ignore2_) -> {});
+                fail("Instance replacement should fail");
+            }
+            catch (Exception e)
+            {
+                assertTrue(e.getMessage().contains("Can not commit 
transformation: \"INVALID\"(Can not add a new in-progress sequence for 
Reconfigure CMS"));
+            }
+
+            // the replacement has failed, unpause node1 to allow it to 
continue with the inflight CMS
+            // reconfiguration, which will succeed.
+            ClusterUtils.unpauseCommits(firstCMS);
+            inFlightReconfig.get().asserts().success();
+        }
+    }
+}
diff --git 
a/test/distributed/org/apache/cassandra/distributed/test/log/TestProcessor.java 
b/test/distributed/org/apache/cassandra/distributed/test/log/TestProcessor.java
index f7f5c50262..8118fe8ae4 100644
--- 
a/test/distributed/org/apache/cassandra/distributed/test/log/TestProcessor.java
+++ 
b/test/distributed/org/apache/cassandra/distributed/test/log/TestProcessor.java
@@ -28,6 +28,7 @@ import java.util.function.Predicate;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import org.apache.cassandra.exceptions.ExceptionCode;
 import org.apache.cassandra.tcm.ClusterMetadata;
 import org.apache.cassandra.tcm.Commit;
 import org.apache.cassandra.tcm.Epoch;
@@ -57,8 +58,22 @@ public class TestProcessor implements Processor
     @Override
     public Commit.Result commit(Entry.Id entryId, Transformation transform, 
Epoch lastKnown, Retry retryPolicy)
     {
-        maybePause(transform);
-        waitIfPaused();
+        if (maybePause(transform))
+        {
+            // Block the calling thread, this happens at most once per 
registered predicate but if there are many
+            // of these the InternalMetadataStage can become saturated by 
paused threads. This can impact other
+            // work, most notably responding to TCM_CURRENT_EPOCH_REQ messages 
issued by ProgressBarrier.
+            waitIfPaused();
+        }
+        else if (isPaused())
+        {
+            // Any commit that arrives while already paused must not park the 
calling thread, so reject immediately
+            // instead, with a retriable (non-rejected) failure. Callers 
should already know how to retry such a
+            // failure.
+            logger.debug("Test processor is paused; rejecting unrelated commit 
of {} instead of blocking", transform.kind());
+            return Commit.Result.failed(ExceptionCode.SERVER_ERROR,
+                                        "TestProcessor is paused for an 
unrelated transformation; retry elsewhere");
+        }
         Commit.Result commited = delegate.commit(entryId, transform, 
lastKnown, retryPolicy);
         testCommitPredicates(transform, commited);
         return commited;
@@ -106,8 +121,9 @@ public class TestProcessor implements Processor
         commitPredicates.add(fn);
     }
 
-    private void maybePause(Transformation transform)
+    private boolean maybePause(Transformation transform)
     {
+        boolean paused = false;
         Iterator<Predicate<Transformation>> iter = waitPredicates.iterator();
 
         while (iter.hasNext())
@@ -116,8 +132,10 @@ public class TestProcessor implements Processor
             {
                 pause();
                 iter.remove();
+                paused = true;
             }
         }
+        return paused;
     }
 
     private void testCommitPredicates(Transformation transform, Commit.Result 
result)
diff --git 
a/test/unit/org/apache/cassandra/tcm/transformations/PrepareLeaveTest.java 
b/test/unit/org/apache/cassandra/tcm/transformations/PrepareLeaveTest.java
index bbf58eda54..aff2f320c2 100644
--- a/test/unit/org/apache/cassandra/tcm/transformations/PrepareLeaveTest.java
+++ b/test/unit/org/apache/cassandra/tcm/transformations/PrepareLeaveTest.java
@@ -55,6 +55,10 @@ import org.apache.cassandra.tcm.ownership.PlacementDeltas;
 import org.apache.cassandra.tcm.ownership.PlacementProvider;
 import org.apache.cassandra.tcm.ownership.PlacementTransitionPlan;
 import org.apache.cassandra.tcm.sequences.LeaveStreams;
+import org.apache.cassandra.tcm.sequences.LockedRanges;
+import org.apache.cassandra.tcm.sequences.ReconfigureCMS;
+import org.apache.cassandra.tcm.transformations.cms.AdvanceCMSReconfiguration;
+import org.apache.cassandra.tcm.transformations.cms.PrepareCMSReconfiguration;
 
 import static 
org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper.addr;
 import static org.junit.Assert.assertFalse;
@@ -106,6 +110,58 @@ public class PrepareLeaveTest
         assertFalse(executeLeave(metadata));
     }
 
+    @Test
+    public void testRejectedWhenLeavingNodeIsCMSReconfigurationAdd() throws 
Throwable
+    {
+        Keyspaces kss = 
Keyspaces.of(DistributedMetadataLogKeyspace.initialMetadata(Sets.newHashSet(hostDc.values())),
 KSM);
+        ClusterMetadata metadata = prepMetadata(kss, 2, 2);
+        NodeId leaving = 
metadata.directory.peerId(InetAddressAndPort.getByName("127.0.0.11"));
+        PrepareCMSReconfiguration.Diff diff = new 
PrepareCMSReconfiguration.Diff(Collections.singletonList(leaving), 
Collections.emptyList());
+        assertFalse(executeLeave(withReconfiguringCMS(metadata, diff)));
+    }
+
+    @Test
+    public void testRejectedWhenLeavingNodeIsCMSReconfigurationRemove() throws 
Throwable
+    {
+        Keyspaces kss = 
Keyspaces.of(DistributedMetadataLogKeyspace.initialMetadata(Sets.newHashSet(hostDc.values())),
 KSM);
+        ClusterMetadata metadata = prepMetadata(kss, 2, 2);
+        NodeId leaving = 
metadata.directory.peerId(InetAddressAndPort.getByName("127.0.0.11"));
+        PrepareCMSReconfiguration.Diff diff = new 
PrepareCMSReconfiguration.Diff(Collections.emptyList(), 
Collections.singletonList(leaving));
+        assertFalse(executeLeave(withReconfiguringCMS(metadata, diff)));
+    }
+
+    private ClusterMetadata withReconfiguringCMS(ClusterMetadata metadata, 
PrepareCMSReconfiguration.Diff diff)
+    {
+        ReconfigureCMS sequence = 
ReconfigureCMS.newSequence(LockedRanges.keyFor(Epoch.FIRST), diff);
+        return metadata.transformer()
+                       
.with(metadata.inProgressSequences.with(ReconfigureCMS.SequenceKey.instance, 
sequence))
+                       .build()
+               .metadata;
+    }
+
+    @Test
+    public void testRejectedWhenLeavingNodeIsActiveCMSTransition() throws 
Throwable
+    {
+        Keyspaces kss = 
Keyspaces.of(DistributedMetadataLogKeyspace.initialMetadata(Sets.newHashSet(hostDc.values())),
 KSM);
+        ClusterMetadata metadata = prepMetadata(kss, 2, 2);
+        NodeId leaving = 
metadata.directory.peerId(InetAddressAndPort.getByName("127.0.0.11"));
+
+        LockedRanges.Key lockKey = LockedRanges.keyFor(Epoch.FIRST);
+        ReconfigureCMS sequence = ReconfigureCMS.newSequence(lockKey, 
PrepareCMSReconfiguration.Diff.NOCHANGE);
+        AdvanceCMSReconfiguration activeStep = new AdvanceCMSReconfiguration(0,
+                                                                             
Epoch.EMPTY,
+                                                                             
lockKey,
+                                                                             
PrepareCMSReconfiguration.Diff.NOCHANGE,
+                                                                             
ReconfigureCMS.ActiveTransition.withNodeIds(leaving, Collections.emptySet()));
+        sequence = sequence.advance(activeStep);
+
+        metadata = metadata.transformer()
+                           
.with(metadata.inProgressSequences.with(ReconfigureCMS.SequenceKey.instance, 
sequence))
+                           .build()
+                   .metadata;
+        assertFalse(executeLeave(metadata));
+    }
+
     private boolean executeLeave(ClusterMetadata metadata) throws Throwable
     {
         PrepareLeave prepareLeave = new 
PrepareLeave(metadata.directory.peerId(InetAddressAndPort.getByName("127.0.0.11")),


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to