Allowing CMS reconfigure if all nodes in the cluster are used

This commit is contained in:
nvharikrishna 2026-07-23 13:49:37 +05:30
parent 1710e84c40
commit c4319a7a0d
2 changed files with 39 additions and 9 deletions

View File

@ -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);
}

View File

@ -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
{