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 feef3fcf51 Fix operationMode reporting DECOMMISSION_FAILED instead of 
LEAVING when resuming a failed decommission
feef3fcf51 is described below

commit feef3fcf512b0c4418c2c7477c80d0bf60f1acc6
Author: Minal Kyada <[email protected]>
AuthorDate: Fri Jul 17 16:53:40 2026 -0700

    Fix operationMode reporting DECOMMISSION_FAILED instead of LEAVING when 
resuming a failed decommission
    
    Patch by Minal Kyada; reviewed by Caleb Rackliffe and Sam Tunnicliffe for 
CASSANDRA-21493
---
 CHANGES.txt                                        |  1 +
 .../tcm/sequences/SingleNodeSequences.java         |  1 +
 .../distributed/test/DecommissionTest.java         |  4 ++-
 .../distributed/test/ring/DecommissionTest.java    | 41 ++++++++++++++++++++++
 4 files changed, 46 insertions(+), 1 deletion(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 1bbeb9076e..09a7cd544f 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 6.0-alpha2
+ * Fix operationMode reporting DECOMMISSION_FAILED instead of LEAVING when 
resuming a failed decommission (CASSANDRA-21493)
  * Avoid megamorphic calls when serializing and deserializing fixed-length 
values (CASSANDRA-21536)
  * Avoid megamorphic calls for 
Cell.timestamp/ttl/path/localDeletionTimeAsUnsignedInt methods (CASSANDRA-21526)
  * Allow unreserved keywords as user and identity names in USER and IDENTITY 
statements (CASSANDRA-21510)
diff --git 
a/src/java/org/apache/cassandra/tcm/sequences/SingleNodeSequences.java 
b/src/java/org/apache/cassandra/tcm/sequences/SingleNodeSequences.java
index 313700aaaf..82a1059b6e 100644
--- a/src/java/org/apache/cassandra/tcm/sequences/SingleNodeSequences.java
+++ b/src/java/org/apache/cassandra/tcm/sequences/SingleNodeSequences.java
@@ -92,6 +92,7 @@ public interface SingleNodeSequences
         else if (InProgressSequences.isLeave(inProgress))
         {
             logger.info("Resuming decommission @ {} (current epoch = {}): {}", 
inProgress.latestModification, metadata.epoch, inProgress.status());
+            StorageService.instance.clearTransientMode();
         }
         else
         {
diff --git 
a/test/distributed/org/apache/cassandra/distributed/test/DecommissionTest.java 
b/test/distributed/org/apache/cassandra/distributed/test/DecommissionTest.java
index ad0120513b..41f8274ae5 100644
--- 
a/test/distributed/org/apache/cassandra/distributed/test/DecommissionTest.java
+++ 
b/test/distributed/org/apache/cassandra/distributed/test/DecommissionTest.java
@@ -21,6 +21,7 @@ package org.apache.cassandra.distributed.test;
 import java.io.IOException;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import net.bytebuddy.ByteBuddy;
 import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
@@ -141,6 +142,7 @@ public class DecommissionTest extends TestBaseImpl
 
     public static class BB
     {
+        static final AtomicBoolean injected = new AtomicBoolean(false);
         public static void install(ClassLoader classLoader, Integer num)
         {
             if (num == 2)
@@ -157,7 +159,7 @@ public class DecommissionTest extends TestBaseImpl
         public static void execute(NodeId leaving, PlacementDeltas startLeave, 
PlacementDeltas midLeave, PlacementDeltas finishLeave,
                                    @SuperCall Callable<?> zuper) throws 
ExecutionException, InterruptedException
         {
-            if (!StorageService.instance.isDecommissionFailed())
+            if (injected.compareAndSet(false, true))
                 throw new ExecutionException(new RuntimeException("simulated 
error in prepareUnbootstrapStreaming"));
 
             try
diff --git 
a/test/distributed/org/apache/cassandra/distributed/test/ring/DecommissionTest.java
 
b/test/distributed/org/apache/cassandra/distributed/test/ring/DecommissionTest.java
index 307956828e..69b8e31430 100644
--- 
a/test/distributed/org/apache/cassandra/distributed/test/ring/DecommissionTest.java
+++ 
b/test/distributed/org/apache/cassandra/distributed/test/ring/DecommissionTest.java
@@ -53,8 +53,10 @@ import org.apache.cassandra.service.StorageService;
 import org.apache.cassandra.streaming.StreamSession;
 import org.apache.cassandra.tcm.ClusterMetadata;
 import org.apache.cassandra.tcm.ClusterMetadataService;
+import org.apache.cassandra.tcm.Epoch;
 import org.apache.cassandra.tcm.membership.NodeId;
 import org.apache.cassandra.tcm.membership.NodeVersion;
+import org.apache.cassandra.tcm.transformations.PrepareLeave;
 import org.apache.cassandra.tcm.transformations.Startup;
 import org.apache.cassandra.utils.CassandraVersion;
 import org.apache.cassandra.utils.FBUtilities;
@@ -62,6 +64,8 @@ import org.apache.cassandra.utils.FBUtilities;
 import static net.bytebuddy.matcher.ElementMatchers.named;
 import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
 import static org.apache.cassandra.distributed.api.Feature.NETWORK;
+import static 
org.apache.cassandra.distributed.shared.ClusterUtils.pauseBeforeCommit;
+import static 
org.apache.cassandra.distributed.shared.ClusterUtils.unpauseCommits;
 import static 
org.apache.cassandra.distributed.shared.NetworkTopology.dcAndRack;
 import static 
org.apache.cassandra.distributed.shared.NetworkTopology.networkTopology;
 import static 
org.apache.cassandra.distributed.test.ring.BootstrapTest.populate;
@@ -84,6 +88,43 @@ public class DecommissionTest extends TestBaseImpl
         }
     }
 
+    @Test
+    public void testOperationModeOnDecomResume() throws Exception
+    {
+        // Node 2's first decomission attempt fails mid-stream (injected by 
BB), leaving it in
+        // DECOMISSION_FAILED. On resume, operationMode() must transition back 
to LEAVING before
+        // MID_LEAVE is committed. We pause the CMS just before that commit to 
assert the mode
+        // in the window where streaming has finished but the epoch has not 
yet advanced.
+        try (Cluster cluster = builder().withNodes(3)
+                                        .withConfig(config -> 
config.with(NETWORK, GOSSIP))
+                                        .withInstanceInitializer(BB::install)
+                                        .start())
+        {
+            populate(cluster, 0, 100, 1, 2, ConsistencyLevel.QUORUM);
+
+            IInvokableInstance cmsNode = cluster.get(1);
+            IInvokableInstance leavingNode = cluster.get(2);
+
+            // --force required: cie_internal keyspace has RF=3, decommission 
would fail replication check otherwise
+            leavingNode.nodetoolResult("decommission", 
"--force").asserts().failure();
+            leavingNode.runOnInstance(() -> 
assertEquals(StorageService.Mode.DECOMMISSION_FAILED, 
StorageService.instance.operationMode()));
+
+            Callable<Epoch> midLeavePaused = pauseBeforeCommit(cmsNode, e -> e 
instanceof PrepareLeave.MidLeave);
+
+            Thread resumeThread = new Thread(() -> 
leavingNode.nodetoolResult("decommission").asserts().success());
+            resumeThread.start();
+            midLeavePaused.call();
+
+            leavingNode.runOnInstance(() ->
+                                      assertEquals("operationMode during 
resumed decommission streaming should be LEAVING, not DECOMMISSION_FAILED",
+                                                   StorageService.Mode.LEAVING,
+                                                   
StorageService.instance.operationMode()));
+
+            unpauseCommits(cmsNode);
+            resumeThread.join(TimeUnit.MINUTES.toMillis(2));
+        }
+    }
+
     @Test
     public void testAddressReuseAfterDecommission() throws IOException, 
ExecutionException, InterruptedException
     {


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

Reply via email to