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 f771e964e3 Add guardrail to enforce minimum CMS size
f771e964e3 is described below

commit f771e964e32ffb9d1b42eaa7dcf14192fc0dbd55
Author: nvharikrishna <[email protected]>
AuthorDate: Mon Jul 20 23:49:12 2026 +0530

    Add guardrail to enforce minimum CMS size
    
    Adds the minimum_cms_size_fail_threshold guardrail that fails an 
operator-requested CMS reconfiguration (nodetool cms reconfigure / JMX) when 
the resulting CMS size (aggregate replication factor across DCs) would fall 
below the threshold.
    
    - Config-driven, default -1 (disabled); valid values are -1 or >= 3
    - Enforced only on operator-initiated reconfiguration, not the automatic 
reconfiguration on topology changes
    - Skipped for clusters smaller than the threshold
    - Fail-only (no warning threshold)
    
    patch by nvharikrishna; reviewed by Stefan Miklosovic and marcuse for 
CASSANDRA-19195
---
 CHANGES.txt                                        |   1 +
 conf/cassandra.yaml                                |   5 +
 conf/cassandra_latest.yaml                         |   5 +
 src/java/org/apache/cassandra/config/Config.java   |   1 +
 .../apache/cassandra/config/GuardrailsOptions.java |  27 ++++
 .../cassandra/db/guardrails/CMSSizeGuardrail.java  |  56 +++++++
 .../apache/cassandra/db/guardrails/Guardrails.java |  18 +++
 .../cassandra/db/guardrails/GuardrailsConfig.java  |   5 +
 .../cassandra/db/guardrails/GuardrailsMBean.java   |  11 ++
 .../org/apache/cassandra/tcm/CMSOperations.java    |  24 ++-
 .../GuardrailCmsSizeReconfigurationTest.java       | 164 +++++++++++++++++++++
 .../db/guardrails/GuardrailMinimumCmsSizeTest.java | 147 ++++++++++++++++++
 .../nodetool/GuardrailsConfigCommandsTest.java     |   9 +-
 13 files changed, 469 insertions(+), 4 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index bc685e1999..67d264132d 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 6.0-alpha3
+ * Add a guardrail to disallow reconfiguring CMS below a minimum size 
(CASSANDRA-19195)
  * Node can report DECOMMISSION_FAILED while actively leaving after a rejected 
decommission attempt (CASSANDRA-21583)
  * Make NoSpamLogger to use Caffeine cache instead of unbounded cache that 
could lead to memory exhaustion (CASSANDRA-21474)
  * Reduce overheads of isOrdinaryUser check performed by Guardrail.enabled 
(CASSANDRA-21589)
diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml
index cba3e84db8..87eb79f53a 100644
--- a/conf/cassandra.yaml
+++ b/conf/cassandra.yaml
@@ -2593,6 +2593,11 @@ drop_compact_storage_enabled: false
 # This would also apply to system keyspaces.
 # maximum_replication_factor_warn_threshold: -1
 # maximum_replication_factor_fail_threshold: -1
+#
+# Guardrail to fail an operator-requested CMS reconfiguration (nodetool cms 
reconfigure / JMX) when the resulting
+# CMS size would be below this threshold. The CMS size is the aggregate 
replication factor across all datacenters.
+# The check is skipped when the cluster has fewer nodes than the threshold. 
Defaults to -1 to disable.
+# minimum_cms_size_fail_threshold: -1
 
 #role_name_policy:
 #  # Implementation class of a validator. When not in form of FQCN, the
diff --git a/conf/cassandra_latest.yaml b/conf/cassandra_latest.yaml
index b33d102340..f48096e8a4 100644
--- a/conf/cassandra_latest.yaml
+++ b/conf/cassandra_latest.yaml
@@ -2364,6 +2364,11 @@ drop_compact_storage_enabled: false
 # This would also apply to system keyspaces.
 # maximum_replication_factor_warn_threshold: -1
 # maximum_replication_factor_fail_threshold: -1
+#
+# Guardrail to fail an operator-requested CMS reconfiguration (nodetool cms 
reconfigure / JMX) when the resulting
+# CMS size would be below this threshold. The CMS size is the aggregate 
replication factor across all datacenters.
+# The check is skipped when the cluster has fewer nodes than the threshold. 
Defaults to -1 to disable.
+# minimum_cms_size_fail_threshold: -1
 
 #role_name_policy:
 #  # Implementation class of a validator. When not in form of FQCN, the
diff --git a/src/java/org/apache/cassandra/config/Config.java 
b/src/java/org/apache/cassandra/config/Config.java
index 9e0bbab808..f6853e0bae 100644
--- a/src/java/org/apache/cassandra/config/Config.java
+++ b/src/java/org/apache/cassandra/config/Config.java
@@ -1053,6 +1053,7 @@ public class Config
     public volatile int minimum_replication_factor_fail_threshold = -1;
     public volatile int maximum_replication_factor_warn_threshold = -1;
     public volatile int maximum_replication_factor_fail_threshold = -1;
+    public volatile int minimum_cms_size_fail_threshold = -1;
     public volatile boolean zero_ttl_on_twcs_warned = true;
     public volatile boolean zero_ttl_on_twcs_enabled = true;
     public volatile boolean non_partition_restricted_index_query_enabled = 
true;
diff --git a/src/java/org/apache/cassandra/config/GuardrailsOptions.java 
b/src/java/org/apache/cassandra/config/GuardrailsOptions.java
index 0d0e3aafb4..73b805ad2c 100644
--- a/src/java/org/apache/cassandra/config/GuardrailsOptions.java
+++ b/src/java/org/apache/cassandra/config/GuardrailsOptions.java
@@ -110,6 +110,7 @@ public class GuardrailsOptions implements GuardrailsConfig
         validateDataDiskUsageMaxDiskSize(config.data_disk_usage_max_disk_size);
         
validateMinRFThreshold(config.minimum_replication_factor_warn_threshold, 
config.minimum_replication_factor_fail_threshold);
         
validateMaxRFThreshold(config.maximum_replication_factor_warn_threshold, 
config.maximum_replication_factor_fail_threshold);
+        validateMinCmsSizeThreshold(config.minimum_cms_size_fail_threshold, 
"minimum_cms_size_fail_threshold");
         validateTimestampThreshold(config.maximum_timestamp_warn_threshold, 
config.maximum_timestamp_fail_threshold, "maximum_timestamp");
         validateTimestampThreshold(config.minimum_timestamp_warn_threshold, 
config.minimum_timestamp_fail_threshold, "minimum_timestamp");
         
validateMaxLongThreshold(config.sai_sstable_indexes_per_query_warn_threshold,
@@ -1051,6 +1052,21 @@ public class GuardrailsOptions implements 
GuardrailsConfig
                                   x -> 
config.minimum_replication_factor_fail_threshold = x);
     }
 
+    @Override
+    public int getMinimumCmsSizeFailThreshold()
+    {
+        return config.minimum_cms_size_fail_threshold;
+    }
+
+    public void setMinimumCmsSizeFailThreshold(int fail)
+    {
+        validateMinCmsSizeThreshold(fail, "minimum_cms_size_fail_threshold");
+        updatePropertyWithLogging("minimum_cms_size_fail_threshold",
+                                  fail,
+                                  () -> config.minimum_cms_size_fail_threshold,
+                                  x -> config.minimum_cms_size_fail_threshold 
= x);
+    }
+
     @Override
     public int getMaximumReplicationFactorWarnThreshold()
     {
@@ -1519,6 +1535,17 @@ public class GuardrailsOptions implements 
GuardrailsConfig
                                                       fail, 
DatabaseDescriptor.getDefaultKeyspaceRF()));
     }
 
+    private static void validateMinCmsSizeThreshold(int value, String name)
+    {
+        if (value == -1)
+            return;
+
+        if (value < 3)
+            throw new IllegalArgumentException(format("Invalid value %d for 
%s: minimum allowed value is 3, " +
+                                                      "as CMS requires at 
least 3 replicas to maintain quorum safely. " +
+                                                      "Use -1 to disable this 
guardrail", value, name));
+    }
+
     public static void 
validateTimestampThreshold(DurationSpec.LongMicrosecondsBound warn,
                                                   
DurationSpec.LongMicrosecondsBound fail,
                                                   String name)
diff --git a/src/java/org/apache/cassandra/db/guardrails/CMSSizeGuardrail.java 
b/src/java/org/apache/cassandra/db/guardrails/CMSSizeGuardrail.java
new file mode 100644
index 0000000000..b404d45fcc
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/guardrails/CMSSizeGuardrail.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.db.guardrails;
+
+import java.util.function.ToLongFunction;
+
+import org.apache.cassandra.service.ClientState;
+
+import static java.lang.String.format;
+
+public class CMSSizeGuardrail extends MinThreshold
+{
+    /**
+     * Creates a new minimum threshold guardrail.
+     *
+     * @param failThreshold   a {@link ClientState}-based provider of the 
value above which the operation should be aborted.
+     */
+    public CMSSizeGuardrail(ToLongFunction<ClientState> failThreshold)
+    {
+        super("minimum_cms_size",
+              null,
+              state -> -1,
+              failThreshold,
+              (isWarning, what, value, threshold) ->
+              format("The CMS size of %s is below the failure threshold of %s. 
" +
+                     "Reconfigure CMS so its total size (sum of replication 
factors across all datacenters) is at least %s.",
+                     value, threshold, threshold));
+        this.throwOnNullClientState = true;
+    }
+
+    public void guard(int totalNodes, int cmsSize)
+    {
+        // If the request already uses every node, the cluster can't do 
better, so skip the check.
+        // Otherwise the CMS size must meet the configured minimum.
+        if (cmsSize >= totalNodes)
+            return;
+
+        guard(cmsSize, "CMS", false, null);
+    }
+}
diff --git a/src/java/org/apache/cassandra/db/guardrails/Guardrails.java 
b/src/java/org/apache/cassandra/db/guardrails/Guardrails.java
index 6a324bff33..e1d1a38b99 100644
--- a/src/java/org/apache/cassandra/db/guardrails/Guardrails.java
+++ b/src/java/org/apache/cassandra/db/guardrails/Guardrails.java
@@ -615,6 +615,12 @@ public final class Guardrails implements GuardrailsMBean
                      format("The keyspace %s has a replication factor of %s, 
below the %s threshold of %s.",
                             what, value, isWarning ? "warning" : "failure", 
threshold));
 
+    /**
+     * Guardrail on the minimum CMS size (aggregate replication factor across 
DCs).
+     */
+    public static final CMSSizeGuardrail minimumCmsSize =
+    new CMSSizeGuardrail(state -> 
CONFIG_PROVIDER.getOrCreate(state).getMinimumCmsSizeFailThreshold());
+
     /**
      * Guardrail on the maximum replication factor.
      */
@@ -1679,6 +1685,18 @@ public final class Guardrails implements GuardrailsMBean
         DEFAULT_CONFIG.setMinimumReplicationFactorThreshold(warn, fail);
     }
 
+    @Override
+    public int getMinimumCmsSizeFailThreshold()
+    {
+        return DEFAULT_CONFIG.getMinimumCmsSizeFailThreshold();
+    }
+
+    @Override
+    public void setMinimumCmsSizeFailThreshold(int fail)
+    {
+        DEFAULT_CONFIG.setMinimumCmsSizeFailThreshold(fail);
+    }
+
     @Override
     public boolean getZeroTTLOnTWCSEnabled()
     {
diff --git a/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java 
b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java
index 5a92c07472..2d8f61a7ae 100644
--- a/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java
+++ b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java
@@ -466,6 +466,11 @@ public interface GuardrailsConfig
      */
     int getMinimumReplicationFactorFailThreshold();
 
+    /**
+     * @return The threshold to fail when the CMS size (aggregate replication 
factor across DCs) is below the threshold.
+     */
+    int getMinimumCmsSizeFailThreshold();
+
     /**
      * @return The threshold to warn when replication factor is greater than 
threshold.
      */
diff --git a/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java 
b/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java
index 69615c78c6..99bf92667e 100644
--- a/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java
+++ b/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java
@@ -910,6 +910,17 @@ public interface GuardrailsMBean
      */
     void setMinimumReplicationFactorThreshold (int warn, int fail);
 
+    /**
+     * @return The threshold to fail when the CMS size (aggregate replication 
factor across DCs) is below the threshold.
+     */
+    int getMinimumCmsSizeFailThreshold();
+
+    /**
+     * @param fail The threshold to fail when the CMS size (aggregate 
replication factor across DCs) is below it.
+     *             -1 means disabled.
+     */
+    void setMinimumCmsSizeFailThreshold(int fail);
+
     /**
      * @return The threshold to fail when replication factor is greater than 
threshold.
      */
diff --git a/src/java/org/apache/cassandra/tcm/CMSOperations.java 
b/src/java/org/apache/cassandra/tcm/CMSOperations.java
index b6df6f8469..3b2e123164 100644
--- a/src/java/org/apache/cassandra/tcm/CMSOperations.java
+++ b/src/java/org/apache/cassandra/tcm/CMSOperations.java
@@ -33,6 +33,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.guardrails.Guardrails;
 import org.apache.cassandra.db.virtual.ClusterMetadataDirectoryTable;
 import org.apache.cassandra.db.virtual.ClusterMetadataLogTable;
 import org.apache.cassandra.schema.ReplicationParams;
@@ -163,13 +164,32 @@ public class CMSOperations implements CMSOperationsMBean
     @Override
     public void reconfigureCMS(int rf)
     {
-        cms.reconfigureCMS(ReplicationParams.simpleMeta(rf, 
ClusterMetadata.current().directory.knownDatacenters()));
+        ReplicationParams params = ReplicationParams.simpleMeta(rf, 
ClusterMetadata.current().directory.knownDatacenters());
+        guardMinimumCmsSize(params);
+        cms.reconfigureCMS(params);
     }
 
     @Override
     public void reconfigureCMS(Map<String, Integer> rf)
     {
-        cms.reconfigureCMS(ReplicationParams.ntsMeta(rf));
+        ReplicationParams params = ReplicationParams.ntsMeta(rf);
+        guardMinimumCmsSize(params);
+        cms.reconfigureCMS(params);
+    }
+
+    /**
+     * Enforces the minimum CMS size guardrail against an operator-requested 
reconfiguration. This is only applied to
+     * explicit reconfigure requests (nodetool / JMX), not to the automatic 
reconfiguration that happens on topology
+     * changes, which reuses the already-committed replication params.
+     */
+    private void guardMinimumCmsSize(ReplicationParams params)
+    {
+        ClusterMetadata metadata = ClusterMetadata.current();
+        int totalNodes = metadata.directory.allJoinedEndpoints().size();
+        int cmsSize = params.options.values().stream()
+                                    .mapToInt(Integer::parseInt)
+                                    .sum();
+        Guardrails.minimumCmsSize.guard(totalNodes, cmsSize);
     }
 
     @Override
diff --git 
a/test/distributed/org/apache/cassandra/distributed/test/guardrails/GuardrailCmsSizeReconfigurationTest.java
 
b/test/distributed/org/apache/cassandra/distributed/test/guardrails/GuardrailCmsSizeReconfigurationTest.java
new file mode 100644
index 0000000000..ecdf76370d
--- /dev/null
+++ 
b/test/distributed/org/apache/cassandra/distributed/test/guardrails/GuardrailCmsSizeReconfigurationTest.java
@@ -0,0 +1,164 @@
+/*
+ * 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.guardrails;
+
+import java.io.IOException;
+import java.util.function.Consumer;
+
+import org.junit.Test;
+
+import org.apache.cassandra.distributed.Cluster;
+import org.apache.cassandra.distributed.api.IInstanceConfig;
+import org.apache.cassandra.distributed.test.TestBaseImpl;
+
+import static org.apache.cassandra.distributed.Cluster.build;
+import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
+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 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
+{
+    private static final int THRESHOLD = 3;
+
+    @Test
+    public void reconfigureBelowThresholdIsRejected() throws IOException
+    {
+        try (Cluster cluster = build(3).withConfig(config(THRESHOLD)).start())
+        {
+            cluster.get(1).nodetoolResult("cms", "reconfigure", "2")
+                   .asserts()
+                   .failure()
+                   .stderrContains("failure threshold of " + THRESHOLD);
+        }
+    }
+
+    @Test
+    public void reconfigureAtThresholdIsAllowed() throws IOException
+    {
+        try (Cluster cluster = build(3).withConfig(config(THRESHOLD)).start())
+        {
+            cluster.get(1).nodetoolResult("cms", "reconfigure", "3")
+                   .asserts()
+                   .success();
+        }
+    }
+
+    @Test
+    public void reconfigureBelowThresholdIsAllowedWhenGuardrailDisabled() 
throws IOException
+    {
+        // Default threshold is -1 (disabled), so a below-safe reconfiguration 
is permitted.
+        try (Cluster cluster = build(3).withConfig(config(null)).start())
+        {
+            cluster.get(1).nodetoolResult("cms", "reconfigure", "2")
+                   .asserts()
+                   .success();
+        }
+    }
+
+    @Test
+    public void reconfigureBelowThresholdIsAllowedWhenClusterTooSmall() throws 
IOException
+    {
+        // 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")
+                   .asserts()
+                   .success();
+        }
+    }
+
+    @Test
+    public void 
reconfigureBelowClusterCapacityIsRejectedWhenThresholdAboveClusterSize() throws 
IOException
+    {
+        // Threshold (4) is above the cluster size (3), but the cluster can 
still host a CMS of 3, so a request for 2
+        // is rejected.
+        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.
+        try (Cluster cluster = build(3).withConfig(config(4)).start())
+        {
+            cluster.get(1).nodetoolResult("cms", "reconfigure", "3")
+                   .asserts()
+                   .success();
+        }
+    }
+
+    @Test
+    public void reconfigureAboveClusterSizeIsNotBlockedByGuardrail() throws 
IOException
+    {
+        // Requested size (5) exceeds the cluster size (3), so the guardrail 
does not apply. The reconfiguration still
+        // fails, but because CMS placement can't find enough nodes, not with 
the guardrail's "failure threshold" error.
+        try (Cluster cluster = build(3).withConfig(config(10)).start())
+        {
+            cluster.get(1).nodetoolResult("cms", "reconfigure", "5")
+                   .asserts()
+                   .failure()
+                   .stderrContains("not enough nodes");
+        }
+    }
+
+    @Test
+    public void reconfigureMultiDcBelowThresholdIsRejected() throws IOException
+    {
+        // 2 DCs x 2 nodes = 4 nodes; requested aggregate size is 1 + 1 = 2, 
below the threshold of 3.
+        try (Cluster cluster = builder().withRacks(2, 1, 
2).withConfig(config(THRESHOLD)).start())
+        {
+            cluster.get(1).nodetoolResult("cms", "reconfigure", 
"datacenter1:1", "datacenter2:1")
+                   .asserts()
+                   .failure()
+                   .stderrContains("failure threshold of " + THRESHOLD);
+        }
+    }
+
+    @Test
+    public void reconfigureMultiDcAtOrAboveThresholdIsAllowed() throws 
IOException
+    {
+        // 2 DCs x 2 nodes = 4 nodes; requested aggregate size is 2 + 2 = 4, 
at or above the threshold of 3.
+        try (Cluster cluster = builder().withRacks(2, 1, 
2).withConfig(config(THRESHOLD)).start())
+        {
+            cluster.get(1).nodetoolResult("cms", "reconfigure", 
"datacenter1:2", "datacenter2:2")
+                   .asserts()
+                   .success();
+        }
+    }
+
+    private static Consumer<IInstanceConfig> config(Integer threshold)
+    {
+        return config -> {
+            config.with(GOSSIP).with(NETWORK);
+            if (threshold != null)
+                config.set("minimum_cms_size_fail_threshold", threshold);
+        };
+    }
+}
diff --git 
a/test/unit/org/apache/cassandra/db/guardrails/GuardrailMinimumCmsSizeTest.java 
b/test/unit/org/apache/cassandra/db/guardrails/GuardrailMinimumCmsSizeTest.java
new file mode 100644
index 0000000000..9c0d504c99
--- /dev/null
+++ 
b/test/unit/org/apache/cassandra/db/guardrails/GuardrailMinimumCmsSizeTest.java
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.db.guardrails;
+
+import java.lang.management.ManagementFactory;
+import java.util.Arrays;
+import java.util.Collection;
+
+import javax.management.JMX;
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.runners.Enclosed;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.GuardrailsOptions;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.assertEquals;
+
+@RunWith(Enclosed.class)
+public class GuardrailMinimumCmsSizeTest
+{
+    private static final int DISABLED = -1;
+
+    /**
+     * Returns a proxy that invokes the Guardrails MBean through the platform 
MBeanServer, so the tests exercise the
+     * real (local, no-network) JMX path rather than a direct method call. The 
MBean is registered defensively in case
+     * MBean registration is disabled in the unit test JVM.
+     */
+    private static GuardrailsMBean guardrailsJmxProxy()
+    {
+        try
+        {
+            MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
+            ObjectName name = new ObjectName(Guardrails.MBEAN_NAME);
+            if (!mbs.isRegistered(name))
+                mbs.registerMBean(Guardrails.instance, name);
+            return JMX.newMBeanProxy(mbs, name, GuardrailsMBean.class);
+        }
+        catch (Exception e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @RunWith(Parameterized.class)
+    public static class ValidValueAcceptedTest
+    {
+        @Parameterized.Parameter
+        public int validValue;
+
+        @Parameterized.Parameters(name = "value={0}")
+        public static Collection<Object[]> validValues()
+        {
+            return Arrays.asList(new Object[][]{ { DISABLED }, { 3 }, { 5 }, { 
7 } });
+        }
+
+        @BeforeClass
+        public static void setupClass()
+        {
+            DatabaseDescriptor.daemonInitialization();
+        }
+
+        @After
+        public void teardown()
+        {
+            
DatabaseDescriptor.getGuardrailsConfig().setMinimumCmsSizeFailThreshold(DISABLED);
+        }
+
+        @Test
+        public void testConfigValidationValueIsAccepted()
+        {
+            GuardrailsOptions guardrails = 
DatabaseDescriptor.getGuardrailsConfig();
+            guardrails.setMinimumCmsSizeFailThreshold(validValue);
+            assertEquals(validValue, 
guardrails.getMinimumCmsSizeFailThreshold());
+        }
+
+        @Test
+        public void testJmxValidationValueIsAccepted()
+        {
+            GuardrailsMBean jmx = guardrailsJmxProxy();
+            jmx.setMinimumCmsSizeFailThreshold(validValue);
+            assertEquals(validValue, jmx.getMinimumCmsSizeFailThreshold());
+        }
+    }
+
+    @RunWith(Parameterized.class)
+    public static class BelowMinimumRejectedTest
+    {
+        @Parameterized.Parameter
+        public int invalidValue;
+
+        @Parameterized.Parameters(name = "value={0}")
+        public static Collection<Object[]> invalidValues()
+        {
+            return Arrays.asList(new Object[][]{ { 2 }, { 1 }, { 0 } });
+        }
+
+        @BeforeClass
+        public static void setupClass()
+        {
+            DatabaseDescriptor.daemonInitialization();
+        }
+
+        @Test
+        public void testConfigValidationBelowMinimumIsRejected()
+        {
+            GuardrailsOptions guardrails = 
DatabaseDescriptor.getGuardrailsConfig();
+            assertThatThrownBy(() -> 
guardrails.setMinimumCmsSizeFailThreshold(invalidValue))
+                      .isInstanceOf(IllegalArgumentException.class)
+                      .hasMessageContaining("minimum allowed value is 3");
+        }
+
+        @Test
+        public void testJmxValidationBelowMinimumIsRejected()
+        {
+            GuardrailsMBean jmx = guardrailsJmxProxy();
+            // The setter throws IllegalArgumentException; the MBeanServer 
wraps it in RuntimeMBeanException, but the
+            // JMX proxy (MBeanServerInvocationHandler) unwraps it back to the 
original exception.
+            assertThatThrownBy(() -> 
jmx.setMinimumCmsSizeFailThreshold(invalidValue))
+                      .isInstanceOf(IllegalArgumentException.class)
+                      .hasMessageContaining("minimum allowed value is 3");
+        }
+    }
+}
diff --git 
a/test/unit/org/apache/cassandra/tools/nodetool/GuardrailsConfigCommandsTest.java
 
b/test/unit/org/apache/cassandra/tools/nodetool/GuardrailsConfigCommandsTest.java
index 12188d6d72..0804719383 100644
--- 
a/test/unit/org/apache/cassandra/tools/nodetool/GuardrailsConfigCommandsTest.java
+++ 
b/test/unit/org/apache/cassandra/tools/nodetool/GuardrailsConfigCommandsTest.java
@@ -172,9 +172,12 @@ public class GuardrailsConfigCommandsTest extends CQLTester
             for (Method method : entry.getValue())
             {
                 String guardrailName = 
GuardrailsConfigCommand.toSnakeCase(method.getName().substring(3));
-                if (entry.getValue().size() == 1)
+                // Thresholds are grouped under a collapsed "<name>_threshold" 
key that intentionally differs from
+                // the field name (e.g. "<name>_fail_threshold"), whether or 
not the guardrail has both warn and
+                // fail halves. Everything else keys directly by its field 
name.
+                boolean isThreshold = method.getName().endsWith("Threshold");
+                if (!isThreshold)
                     assertEquals(entry.getKey(), guardrailName);
-                // else it is threshold, so it does not match the key
 
                 // assert converted snake-case guardrail name is actually in 
Config / cassandra.yaml
                 assertTrue(configFieldNames.contains(guardrailName));
@@ -238,6 +241,7 @@ public class GuardrailsConfigCommandsTest extends CQLTester
     "materialized_views_per_table_threshold       [-1, -1]      \n" +
     "maximum_replication_factor_threshold         [-1, -1]      \n" +
     "maximum_timestamp_threshold                  [null, null]  \n" +
+    "minimum_cms_size_threshold                   -1            \n" +
     "minimum_replication_factor_threshold         [-1, -1]      \n" +
     "minimum_timestamp_threshold                  [null, null]  \n" +
     "page_size_threshold                          [-1, -1]      \n" +
@@ -288,6 +292,7 @@ public class GuardrailsConfigCommandsTest extends CQLTester
     "maximum_replication_factor_warn_threshold         -1   \n" +
     "maximum_timestamp_fail_threshold                  null \n" +
     "maximum_timestamp_warn_threshold                  null \n" +
+    "minimum_cms_size_fail_threshold                   -1   \n" +
     "minimum_replication_factor_fail_threshold         -1   \n" +
     "minimum_replication_factor_warn_threshold         -1   \n" +
     "minimum_timestamp_fail_threshold                  null \n" +


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

Reply via email to