From a5569da3c9cb7b373cbbb9a67121497663c36769 Mon Sep 17 00:00:00 2001 From: Arvind Kandpal Date: Fri, 24 Jul 2026 13:48:29 +0530 Subject: [PATCH 1/6] CASSANDRA-14366: Add prepared statement cache stats to nodetool info Assisted-by: Claude Sonnet 4.6 (1M context) --- .../apache/cassandra/metrics/CQLMetrics.java | 2 + .../org/apache/cassandra/tools/NodeProbe.java | 32 +++++++++++++ .../apache/cassandra/tools/nodetool/Info.java | 9 ++++ .../mock/nodetool/InternalNodeProbe.java | 6 +++ .../cassandra/tools/nodetool/InfoTest.java | 46 +++++++++++++++++++ 5 files changed, 95 insertions(+) create mode 100644 test/unit/org/apache/cassandra/tools/nodetool/InfoTest.java diff --git a/src/java/org/apache/cassandra/metrics/CQLMetrics.java b/src/java/org/apache/cassandra/metrics/CQLMetrics.java index e7b6ec57d18b..4a134c9a4238 100644 --- a/src/java/org/apache/cassandra/metrics/CQLMetrics.java +++ b/src/java/org/apache/cassandra/metrics/CQLMetrics.java @@ -39,6 +39,7 @@ public class CQLMetrics public final Gauge preparedStatementsCount; public final Gauge preparedStatementsRatio; public final Gauge preparedStatementsCacheSize; + public final Gauge preparedStatementsCacheCapacity; public CQLMetrics() { @@ -67,5 +68,6 @@ public double getDenominator() } }); preparedStatementsCacheSize = Metrics.register(factory.createMetricName("PreparedStatementsCacheSize"), QueryProcessor::preparedStatementsCacheMemoryUsedBytes); + preparedStatementsCacheCapacity = Metrics.register(factory.createMetricName("PreparedStatementsCacheCapacity"), () -> QueryProcessor.PREPARED_STATEMENT_CACHE_SIZE_BYTES); } } diff --git a/src/java/org/apache/cassandra/tools/NodeProbe.java b/src/java/org/apache/cassandra/tools/NodeProbe.java index c60e0427e59b..793ccb1e7021 100644 --- a/src/java/org/apache/cassandra/tools/NodeProbe.java +++ b/src/java/org/apache/cassandra/tools/NodeProbe.java @@ -1939,6 +1939,38 @@ public Object getBufferPoolMetric(String poolType, String metricName) } } + /** + * Retrieve CQL metrics by name. + * @param metricName PreparedStatementsCount, PreparedStatementsCacheSize, PreparedStatementsCacheCapacity, + * PreparedStatementsExecuted, or PreparedStatementsEvicted. + */ + public Object getCQLMetric(String metricName) + { + try + { + switch (metricName) + { + case "PreparedStatementsCount": + case "PreparedStatementsCacheSize": + case "PreparedStatementsCacheCapacity": + return JMX.newMBeanProxy(mbeanServerConn, + new ObjectName("org.apache.cassandra.metrics:type=CQL,name=" + metricName), + CassandraMetricsRegistry.JmxGaugeMBean.class).getValue(); + case "PreparedStatementsExecuted": + case "PreparedStatementsEvicted": + return JMX.newMBeanProxy(mbeanServerConn, + new ObjectName("org.apache.cassandra.metrics:type=CQL,name=" + metricName), + CassandraMetricsRegistry.JmxCounterMBean.class).getCount(); + default: + throw new RuntimeException("Unknown CQL metric name " + metricName); + } + } + catch (MalformedObjectNameException e) + { + throw new RuntimeException(e); + } + } + private static Multimap getJmxThreadPools(MBeanServerConnection mbeanServerConn) { try diff --git a/src/java/org/apache/cassandra/tools/nodetool/Info.java b/src/java/org/apache/cassandra/tools/nodetool/Info.java index b0b2dff019ab..dc721c7ddd20 100644 --- a/src/java/org/apache/cassandra/tools/nodetool/Info.java +++ b/src/java/org/apache/cassandra/tools/nodetool/Info.java @@ -119,6 +119,15 @@ public void execute(NodeProbe probe) probe.getCacheMetric("CounterCache", "HitRate"), cacheService.getCounterCacheSavePeriodInSeconds()); + // Prepared Statement Cache: entries, size, capacity, executions, evictions + out.printf("%-23s: entries %d, size %s, capacity %s, %d executions, %d evictions%n", + "Prepared Stmt Cache", + probe.getCQLMetric("PreparedStatementsCount"), + FileUtils.stringifyFileSize((long) probe.getCQLMetric("PreparedStatementsCacheSize")), + FileUtils.stringifyFileSize((long) probe.getCQLMetric("PreparedStatementsCacheCapacity")), + probe.getCQLMetric("PreparedStatementsExecuted"), + probe.getCQLMetric("PreparedStatementsEvicted")); + // Chunk Cache: Hits, Requests, RecentHitRate, SavePeriodInSeconds try { diff --git a/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbe.java b/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbe.java index 62e0dad3ea1d..78183f7d2bd8 100644 --- a/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbe.java +++ b/test/distributed/org/apache/cassandra/distributed/mock/nodetool/InternalNodeProbe.java @@ -174,6 +174,12 @@ public Object getCompactionMetric(String metricName) throw new UnsupportedOperationException(); } + @Override + public Object getCQLMetric(String metricName) + { + throw new UnsupportedOperationException(); + } + @Override public Object getClientMetric(String metricName) { diff --git a/test/unit/org/apache/cassandra/tools/nodetool/InfoTest.java b/test/unit/org/apache/cassandra/tools/nodetool/InfoTest.java new file mode 100644 index 000000000000..e7597de2b9ce --- /dev/null +++ b/test/unit/org/apache/cassandra/tools/nodetool/InfoTest.java @@ -0,0 +1,46 @@ +/* + * 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.tools.nodetool; + +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.tools.ToolRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +public class InfoTest extends CQLTester +{ + @BeforeClass + public static void setup() throws Exception + { + requireNetwork(); + startJMXServer(); + } + + @Test + public void testInfoContainsPreparedStmtCache() + { + ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("info"); + tool.assertOnCleanExit(); + String stdout = tool.getStdout(); + assertThat(stdout).contains("Prepared Stmt Cache"); + assertThat(stdout).containsPattern("Prepared Stmt Cache\\s+: entries \\d+, size .+, capacity .+, \\d+ executions, \\d+ evictions"); + } +} From 3540e2463fb98ff3f0ca9f46a67f0e2784264feb Mon Sep 17 00:00:00 2001 From: Arvind Kandpal Date: Tue, 28 Jul 2026 11:55:04 +0530 Subject: [PATCH 2/6] CASSANDRA-14366: Add try-catch block for missing prepared statement --- .../apache/cassandra/tools/nodetool/Info.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/java/org/apache/cassandra/tools/nodetool/Info.java b/src/java/org/apache/cassandra/tools/nodetool/Info.java index dc721c7ddd20..f312385ecfd1 100644 --- a/src/java/org/apache/cassandra/tools/nodetool/Info.java +++ b/src/java/org/apache/cassandra/tools/nodetool/Info.java @@ -120,13 +120,21 @@ public void execute(NodeProbe probe) cacheService.getCounterCacheSavePeriodInSeconds()); // Prepared Statement Cache: entries, size, capacity, executions, evictions - out.printf("%-23s: entries %d, size %s, capacity %s, %d executions, %d evictions%n", - "Prepared Stmt Cache", - probe.getCQLMetric("PreparedStatementsCount"), - FileUtils.stringifyFileSize((long) probe.getCQLMetric("PreparedStatementsCacheSize")), - FileUtils.stringifyFileSize((long) probe.getCQLMetric("PreparedStatementsCacheCapacity")), - probe.getCQLMetric("PreparedStatementsExecuted"), - probe.getCQLMetric("PreparedStatementsEvicted")); + try + { + out.printf("%-23s: entries %d, size %s, capacity %s, %d executions, %d evictions%n", + "Prepared Stmt Cache", + probe.getCQLMetric("PreparedStatementsCount"), + FileUtils.stringifyFileSize((long) probe.getCQLMetric("PreparedStatementsCacheSize")), + FileUtils.stringifyFileSize((long) probe.getCQLMetric("PreparedStatementsCacheCapacity")), + probe.getCQLMetric("PreparedStatementsExecuted"), + probe.getCQLMetric("PreparedStatementsEvicted")); + } + catch (RuntimeException e) + { + if (!(e.getCause() instanceof InstanceNotFoundException)) + throw e; + } // Chunk Cache: Hits, Requests, RecentHitRate, SavePeriodInSeconds try From efa83680c2b387b0b37bf02dc424564592775541 Mon Sep 17 00:00:00 2001 From: Arvind Kandpal Date: Thu, 30 Jul 2026 11:47:34 +0530 Subject: [PATCH 3/6] CASSANDRA-14366: Remove hardcoded switch case and fetch CQL metrics dynamically --- .../org/apache/cassandra/tools/NodeProbe.java | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/java/org/apache/cassandra/tools/NodeProbe.java b/src/java/org/apache/cassandra/tools/NodeProbe.java index 793ccb1e7021..c6e774f5aaea 100644 --- a/src/java/org/apache/cassandra/tools/NodeProbe.java +++ b/src/java/org/apache/cassandra/tools/NodeProbe.java @@ -44,11 +44,16 @@ import java.util.concurrent.TimeoutException; import javax.annotation.Nullable; +import javax.management.AttributeNotFoundException; import javax.management.InstanceNotFoundException; +import javax.management.IntrospectionException; import javax.management.JMX; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanException; import javax.management.MBeanServerConnection; import javax.management.MalformedObjectNameException; import javax.management.ObjectName; +import javax.management.ReflectionException; import javax.management.openmbean.CompositeData; import javax.management.openmbean.OpenDataException; import javax.management.openmbean.TabularData; @@ -112,7 +117,9 @@ import org.apache.cassandra.locator.EndpointSnitchInfoMBean; import org.apache.cassandra.locator.LocationInfoMBean; import org.apache.cassandra.metrics.CIDRAuthorizerMetrics; +import org.apache.cassandra.metrics.CQLMetrics; import org.apache.cassandra.metrics.CassandraMetricsRegistry; +import org.apache.cassandra.metrics.DefaultNameFactory; import org.apache.cassandra.metrics.StorageMetrics; import org.apache.cassandra.metrics.TableMetrics; import org.apache.cassandra.metrics.ThreadPoolMetrics; @@ -1940,32 +1947,25 @@ public Object getBufferPoolMetric(String poolType, String metricName) } /** - * Retrieve CQL metrics by name. - * @param metricName PreparedStatementsCount, PreparedStatementsCacheSize, PreparedStatementsCacheCapacity, - * PreparedStatementsExecuted, or PreparedStatementsEvicted. + * Retrieve a CQL metric value by name. Works generically for any metric registered under + * {@code org.apache.cassandra.metrics:type=CQL,name=} by inspecting the MBean + * attributes at runtime, without requiring knowledge of the underlying metric type. */ public Object getCQLMetric(String metricName) { try { - switch (metricName) + ObjectName objectName = new ObjectName(DefaultNameFactory.GROUP_NAME + ":type=" + CQLMetrics.TYPE_NAME + ",name=" + metricName); + for (MBeanAttributeInfo attr : mbeanServerConn.getMBeanInfo(objectName).getAttributes()) { - case "PreparedStatementsCount": - case "PreparedStatementsCacheSize": - case "PreparedStatementsCacheCapacity": - return JMX.newMBeanProxy(mbeanServerConn, - new ObjectName("org.apache.cassandra.metrics:type=CQL,name=" + metricName), - CassandraMetricsRegistry.JmxGaugeMBean.class).getValue(); - case "PreparedStatementsExecuted": - case "PreparedStatementsEvicted": - return JMX.newMBeanProxy(mbeanServerConn, - new ObjectName("org.apache.cassandra.metrics:type=CQL,name=" + metricName), - CassandraMetricsRegistry.JmxCounterMBean.class).getCount(); - default: - throw new RuntimeException("Unknown CQL metric name " + metricName); + String name = attr.getName(); + if ("Value".equals(name) || "Count".equals(name)) + return mbeanServerConn.getAttribute(objectName, name); } + throw new RuntimeException("No readable value attribute for CQL metric: " + metricName); } - catch (MalformedObjectNameException e) + catch (MalformedObjectNameException | InstanceNotFoundException | IntrospectionException | + ReflectionException | AttributeNotFoundException | MBeanException | IOException e) { throw new RuntimeException(e); } From 1caf2318e62c4cf7e3ffb50e8d462c3a95803389 Mon Sep 17 00:00:00 2001 From: Arvind Kandpal Date: Thu, 30 Jul 2026 15:23:26 +0530 Subject: [PATCH 4/6] CASSANDRA-14366: Add test assertions for non-zero prepared statement metrics --- .../cassandra/tools/nodetool/InfoTest.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/unit/org/apache/cassandra/tools/nodetool/InfoTest.java b/test/unit/org/apache/cassandra/tools/nodetool/InfoTest.java index e7597de2b9ce..0b5db9a47c65 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/InfoTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/InfoTest.java @@ -17,6 +17,10 @@ */ package org.apache.cassandra.tools.nodetool; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.datastax.driver.core.PreparedStatement; import org.junit.BeforeClass; import org.junit.Test; @@ -27,6 +31,9 @@ public class InfoTest extends CQLTester { + private static final Pattern PREPARED_STMT_CACHE_PATTERN = + Pattern.compile("Prepared Stmt Cache\\s+: entries (\\d+), size ([^,]+), capacity ([^,]+), (\\d+) executions, (\\d+) evictions"); + @BeforeClass public static void setup() throws Exception { @@ -37,10 +44,18 @@ public static void setup() throws Exception @Test public void testInfoContainsPreparedStmtCache() { + createTable("CREATE TABLE %s (id int PRIMARY KEY, val text)"); + PreparedStatement preparedStatement = sessionNet().prepare("INSERT INTO " + KEYSPACE + '.' + currentTable() + " (id, val) VALUES (?, ?)"); + sessionNet().execute(preparedStatement.bind(1, "value1")); + ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("info"); tool.assertOnCleanExit(); String stdout = tool.getStdout(); assertThat(stdout).contains("Prepared Stmt Cache"); - assertThat(stdout).containsPattern("Prepared Stmt Cache\\s+: entries \\d+, size .+, capacity .+, \\d+ executions, \\d+ evictions"); + Matcher matcher = PREPARED_STMT_CACHE_PATTERN.matcher(stdout); + assertThat(matcher.find()).isTrue(); + assertThat(Integer.parseInt(matcher.group(1))).isGreaterThan(0); + assertThat(matcher.group(2)).isNotEqualTo("0 bytes"); + assertThat(Integer.parseInt(matcher.group(4))).isGreaterThan(0); } } From b60aaf7996a9e3c8b8226fb4d4aa7edf3d3559ff Mon Sep 17 00:00:00 2001 From: Arvind Kandpal Date: Thu, 30 Jul 2026 16:17:23 +0530 Subject: [PATCH 5/6] CASSANDRA-14366: Use full word 'Statement' and align printf arguments --- .../org/apache/cassandra/tools/nodetool/Info.java | 12 ++++++------ .../apache/cassandra/tools/nodetool/InfoTest.java | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/java/org/apache/cassandra/tools/nodetool/Info.java b/src/java/org/apache/cassandra/tools/nodetool/Info.java index f312385ecfd1..381b3ec44a47 100644 --- a/src/java/org/apache/cassandra/tools/nodetool/Info.java +++ b/src/java/org/apache/cassandra/tools/nodetool/Info.java @@ -123,12 +123,12 @@ public void execute(NodeProbe probe) try { out.printf("%-23s: entries %d, size %s, capacity %s, %d executions, %d evictions%n", - "Prepared Stmt Cache", - probe.getCQLMetric("PreparedStatementsCount"), - FileUtils.stringifyFileSize((long) probe.getCQLMetric("PreparedStatementsCacheSize")), - FileUtils.stringifyFileSize((long) probe.getCQLMetric("PreparedStatementsCacheCapacity")), - probe.getCQLMetric("PreparedStatementsExecuted"), - probe.getCQLMetric("PreparedStatementsEvicted")); + "Prepared Statement Cache", + probe.getCQLMetric("PreparedStatementsCount"), + FileUtils.stringifyFileSize((long) probe.getCQLMetric("PreparedStatementsCacheSize")), + FileUtils.stringifyFileSize((long) probe.getCQLMetric("PreparedStatementsCacheCapacity")), + probe.getCQLMetric("PreparedStatementsExecuted"), + probe.getCQLMetric("PreparedStatementsEvicted")); } catch (RuntimeException e) { diff --git a/test/unit/org/apache/cassandra/tools/nodetool/InfoTest.java b/test/unit/org/apache/cassandra/tools/nodetool/InfoTest.java index 0b5db9a47c65..96c1018643f0 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/InfoTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/InfoTest.java @@ -31,8 +31,8 @@ public class InfoTest extends CQLTester { - private static final Pattern PREPARED_STMT_CACHE_PATTERN = - Pattern.compile("Prepared Stmt Cache\\s+: entries (\\d+), size ([^,]+), capacity ([^,]+), (\\d+) executions, (\\d+) evictions"); + private static final Pattern PREPARED_STATEMENT_CACHE_PATTERN = + Pattern.compile("Prepared Statement Cache\\s+: entries (\\d+), size ([^,]+), capacity ([^,]+), (\\d+) executions, (\\d+) evictions"); @BeforeClass public static void setup() throws Exception @@ -42,7 +42,7 @@ public static void setup() throws Exception } @Test - public void testInfoContainsPreparedStmtCache() + public void testInfoContainsPreparedStatementCache() { createTable("CREATE TABLE %s (id int PRIMARY KEY, val text)"); PreparedStatement preparedStatement = sessionNet().prepare("INSERT INTO " + KEYSPACE + '.' + currentTable() + " (id, val) VALUES (?, ?)"); @@ -51,8 +51,8 @@ public void testInfoContainsPreparedStmtCache() ToolRunner.ToolResult tool = ToolRunner.invokeNodetool("info"); tool.assertOnCleanExit(); String stdout = tool.getStdout(); - assertThat(stdout).contains("Prepared Stmt Cache"); - Matcher matcher = PREPARED_STMT_CACHE_PATTERN.matcher(stdout); + assertThat(stdout).contains("Prepared Statement Cache"); + Matcher matcher = PREPARED_STATEMENT_CACHE_PATTERN.matcher(stdout); assertThat(matcher.find()).isTrue(); assertThat(Integer.parseInt(matcher.group(1))).isGreaterThan(0); assertThat(matcher.group(2)).isNotEqualTo("0 bytes"); From e63745004dadba9cb19bea6ea2c66b8e791699dc Mon Sep 17 00:00:00 2001 From: Arvind Kandpal Date: Thu, 30 Jul 2026 16:34:02 +0530 Subject: [PATCH 6/6] CASSANDRA-14366: Fix checkstyle import order in InfoTest.java --- test/unit/org/apache/cassandra/tools/nodetool/InfoTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/org/apache/cassandra/tools/nodetool/InfoTest.java b/test/unit/org/apache/cassandra/tools/nodetool/InfoTest.java index 96c1018643f0..9097cb9229ca 100644 --- a/test/unit/org/apache/cassandra/tools/nodetool/InfoTest.java +++ b/test/unit/org/apache/cassandra/tools/nodetool/InfoTest.java @@ -21,6 +21,7 @@ import java.util.regex.Pattern; import com.datastax.driver.core.PreparedStatement; + import org.junit.BeforeClass; import org.junit.Test;