From c4319a7a0d67e0a2219be43fa49a76260486b8dd Mon Sep 17 00:00:00 2001 From: nvharikrishna Date: Thu, 23 Jul 2026 13:49:37 +0530 Subject: [PATCH] Allowing CMS reconfigure if all nodes in the cluster are used --- .../apache/cassandra/tcm/CMSOperations.java | 14 ++++---- .../GuardrailCmsSizeReconfigurationTest.java | 34 +++++++++++++++++-- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/java/org/apache/cassandra/tcm/CMSOperations.java b/src/java/org/apache/cassandra/tcm/CMSOperations.java index e013150d1b..089ca93c7f 100644 --- a/src/java/org/apache/cassandra/tcm/CMSOperations.java +++ b/src/java/org/apache/cassandra/tcm/CMSOperations.java @@ -184,16 +184,18 @@ public class CMSOperations implements CMSOperationsMBean private void guardMinimumCmsSize(ReplicationParams params) { ClusterMetadata metadata = ClusterMetadata.current(); - int minCmsSize = Guardrails.instance.getMinimumCmsSizeFailThreshold(); int totalNodes = metadata.directory.allJoinedEndpoints().size(); - // Skip clusters smaller than the threshold: they cannot host that many replicas yet (e.g. during bootstrap). - // When the guardrail is disabled this is never true, and guard() below is then a no-op. - if (totalNodes < minCmsSize) - return; - int cmsSize = params.options.values().stream() .mapToInt(Integer::parseInt) .sum(); + // A cluster can't host more CMS replicas than it has nodes. If the request already uses every node there's + // nothing more to enforce (e.g. during bootstrap), and requiring more would brick reconfiguration. Otherwise + // the request must meet the configured minimum, even when the threshold exceeds the current cluster size, so + // an operator can't sidestep the floor by setting a high threshold. An operator who genuinely wants a smaller + // CMS should disable the guardrail. When it is disabled, guard() below is a no-op. + if (cmsSize >= totalNodes) + return; + Guardrails.minimumCmsSize.guard(cmsSize, "CMS", false, null); } diff --git a/test/distributed/org/apache/cassandra/distributed/test/guardrails/GuardrailCmsSizeReconfigurationTest.java b/test/distributed/org/apache/cassandra/distributed/test/guardrails/GuardrailCmsSizeReconfigurationTest.java index 321a6ab136..e99f15f0ec 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/guardrails/GuardrailCmsSizeReconfigurationTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/guardrails/GuardrailCmsSizeReconfigurationTest.java @@ -33,8 +33,8 @@ import static org.apache.cassandra.distributed.api.Feature.NETWORK; /** * Exercises the minimum CMS size guardrail via the real {@code nodetool cms reconfigure} path against a live in-JVM - * cluster. The guardrail only applies once the cluster has at least as many nodes as the threshold, so multiple nodes - * are required to trigger it. + * cluster. The guardrail applies unless the requested CMS size already uses every joined node (the cluster cannot + * host more replicas), so multiple nodes are required to trigger it. */ public class GuardrailCmsSizeReconfigurationTest extends TestBaseImpl { @@ -78,7 +78,7 @@ public class GuardrailCmsSizeReconfigurationTest extends TestBaseImpl @Test public void reconfigureBelowThresholdIsAllowedWhenClusterTooSmall() throws IOException { - // The cluster has fewer nodes than the threshold, so it cannot host that many replicas and the guardrail is skipped. + // The requested CMS size (2) already uses every node in the cluster, so there is nothing more to enforce. try (Cluster cluster = build(2).withConfig(config(THRESHOLD)).start()) { cluster.get(1).nodetoolResult("cms", "reconfigure", "2") @@ -87,6 +87,34 @@ public class GuardrailCmsSizeReconfigurationTest extends TestBaseImpl } } + @Test + public void reconfigureBelowClusterCapacityIsRejectedWhenThresholdAboveClusterSize() throws IOException + { + // Threshold (4) is above the cluster size (3), but the cluster can still host a CMS of 3. Requesting a smaller + // size than the cluster can support must be rejected rather than silently skipped, so a high threshold can't + // be used to sidestep the floor. + try (Cluster cluster = build(3).withConfig(config(4)).start()) + { + cluster.get(1).nodetoolResult("cms", "reconfigure", "2") + .asserts() + .failure() + .stderrContains("failure threshold of 4"); + } + } + + @Test + public void reconfigureAtClusterCapacityIsAllowedWhenThresholdAboveClusterSize() throws IOException + { + // Threshold (4) is above the cluster size (3). A CMS of 3 already uses every node, so it is allowed even though + // it is below the threshold: the cluster simply cannot do better, and blocking it would brick reconfiguration. + try (Cluster cluster = build(3).withConfig(config(4)).start()) + { + cluster.get(1).nodetoolResult("cms", "reconfigure", "3") + .asserts() + .success(); + } + } + @Test public void reconfigureMultiDcBelowThresholdIsRejected() throws IOException {