This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 0ca062d92 fix(nameserver): report unreachable endpoints in the config 
read tool (#5279)
0ca062d92 is described below

commit 0ca062d929a22c62c503e07f68dc33f5c2992f68
Author: Apulupie <[email protected]>
AuthorDate: Fri Oct 9 19:30:44 2026 +0800

    fix(nameserver): report unreachable endpoints in the config read tool 
(#5279)
    
    `NameServerConfigDiffService.read()` logged and skipped a NameServer 
endpoint it could not reach and
    returned the reachable subset as if it were the whole picture, so the AI 
tool
    `rmq.nameserver.config.diff` reported a partial read as complete. The 
result now carries the
    unreachable endpoints and sets `resultMayBeTruncated`, and the tool output 
schema documents both.
    
    `tool-catalog/tools/nameserver.yaml` changed, so 
`rmqctl/internal/catalog/catalog_gen.go` was
    regenerated with `make catalog-generate`; `make catalog-verify` reports no 
drift
    (digest `66dfbf74…`), `go test ./internal/catalog/...` is green and `make 
fmt` is clean.
    `NameServerConfigDiffServiceTest` + `NameServerConfigToolHandlerTest` + 
`ToolOutputSchemaContractTest`:
    18 tests green, 0 checkstyle violations. Both new behaviours were 
mutation-checked: dropping the
    `unreachable.add(address)` call reddens the skip case, and hardcoding 
`resultMayBeTruncated` to false
    reddens the coverage case.
---
 rmqctl/internal/catalog/catalog_gen.go             |  2 +-
 .../nameserver/NameServerConfigDiffService.java    | 15 +++++++--
 .../nameserver/NameserverConfigOutput.java         | 38 ++++++++++++++++++++++
 .../nameserver/NameServerConfigToolHandler.java    | 12 ++++---
 .../resources/tool-catalog/tools/nameserver.yaml   | 14 ++++++++
 .../NameServerConfigDiffServiceTest.java           | 13 ++++----
 .../NameServerConfigToolHandlerTest.java           | 31 +++++++++++++++---
 .../tool/service/ToolOutputSchemaContractTest.java |  7 ++--
 8 files changed, 111 insertions(+), 21 deletions(-)

diff --git a/rmqctl/internal/catalog/catalog_gen.go 
b/rmqctl/internal/catalog/catalog_gen.go
index df9af561a..187280e97 100644
--- a/rmqctl/internal/catalog/catalog_gen.go
+++ b/rmqctl/internal/catalog/catalog_gen.go
@@ -21,7 +21,7 @@ package catalog
 var defaultDocument = Document{
        Version:              "2.0.0",
        MinimumClientVersion: "2.0.0",
-       Digest:               
"098b84cd35cfe2dde3b71a1d4381863a54e187517564bbe004b6ea329e8f1814",
+       Digest:               
"66dfbf74525c8efb00eb4752a5b569c06b024ea549861b306cd4d8bf8962c34e",
        Tools: []Tool{
                {
                        Name:                 "rmq.acl.list",
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffService.java
index 1cc7255a9..fce482b75 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffService.java
@@ -151,7 +151,7 @@ public class NameServerConfigDiffService {
      * (fixes the §15.5.5 "Cluster details are unavailable" path that keyed on 
the instance id).
      * Secret-bearing keys are never exposed: only {@link #SAFE_CONFIG_KEYS} 
are returned.
      */
-    public List<NodeConfig> read(String clusterId, String instanceId) {
+    public NameServerConfigRead read(String clusterId, String instanceId) {
         String normalizedClusterId = requireClusterId(clusterId);
         String normalizedInstanceId = normalizeInstanceId(instanceId);
         ClusterVO cluster = normalizedInstanceId == null
@@ -164,11 +164,13 @@ public class NameServerConfigDiffService {
         }
         String connectionEndpoint = connectionEndpoint(cluster, addresses);
         List<NodeConfig> read = new ArrayList<>();
+        List<String> unreachable = new ArrayList<>();
         for (String address : addresses) {
             try {
                 Properties config = readConfig(normalizedInstanceId, 
connectionEndpoint, address);
                 read.add(new NodeConfig(address, safeConfig(config)));
             } catch (BusinessException exception) {
+                unreachable.add(address);
                 log.warn("Skipping unreachable NameServer {} while reading 
config for cluster {}: {}",
                         address, normalizedClusterId, exception.getMessage());
             }
@@ -177,7 +179,7 @@ public class NameServerConfigDiffService {
             throw new BusinessException(502,
                     "No reachable NameServer endpoint to read config from: " + 
normalizedClusterId);
         }
-        return read;
+        return new NameServerConfigRead(List.copyOf(read), 
List.copyOf(unreachable));
     }
 
     private Map<String, String> safeConfig(Properties config) {
@@ -195,6 +197,15 @@ public class NameServerConfigDiffService {
     public record NodeConfig(String addr, Map<String, String> config) {
     }
 
+    /**
+     * Result of {@link #read}: the safe configuration of every reachable 
endpoint plus the
+     * addresses that could not be read. Callers surface the unreachable set 
instead of silently
+     * presenting a partial view as complete; {@code unreachableEndpoints} is 
empty when every
+     * endpoint answered.
+     */
+    public record NameServerConfigRead(List<NodeConfig> nodes, List<String> 
unreachableEndpoints) {
+    }
+
     private Properties readConfig(String instanceId, String 
connectionEndpoint, String address) {
         if (instanceId != null) {
             return runtimeAdminClientResolver.execute(instanceId, admin -> 
readConfig(admin, address));
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/nameserver/NameserverConfigOutput.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/nameserver/NameserverConfigOutput.java
new file mode 100644
index 000000000..86eac38e5
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/nameserver/NameserverConfigOutput.java
@@ -0,0 +1,38 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.contract.nameserver;
+
+import java.util.List;
+
+/**
+ * Output of {@code rmq.nameserver.config}. {@code items} covers only the 
endpoints that answered;
+ * {@code resultMayBeTruncated} is true when some NameServer endpoints could 
not be read and
+ * {@code unreachableEndpoints} names them, so the caller can weigh the 
partial coverage instead of
+ * mistaking it for the full cluster configuration.
+ */
+public record NameserverConfigOutput(
+        List<NameserverConfigItem> items,
+        boolean resultMayBeTruncated,
+        List<String> unreachableEndpoints) {
+
+    public static NameserverConfigOutput of(List<NameserverConfigItem> items, 
List<String> unreachableEndpoints) {
+        return new NameserverConfigOutput(
+                List.copyOf(items),
+                !unreachableEndpoints.isEmpty(),
+                List.copyOf(unreachableEndpoints));
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/nameserver/NameServerConfigToolHandler.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/nameserver/NameServerConfigToolHandler.java
index b63099b7f..7e53cb3e6 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/nameserver/NameServerConfigToolHandler.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/nameserver/NameServerConfigToolHandler.java
@@ -17,9 +17,9 @@
 package org.apache.rocketmq.studio.ops.ai.tool.handler.nameserver;
 
 import 
org.apache.rocketmq.studio.cluster.nameserver.NameServerConfigDiffService;
-import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ListOutput;
 import 
org.apache.rocketmq.studio.ops.ai.tool.contract.nameserver.NameserverConfigInput;
 import 
org.apache.rocketmq.studio.ops.ai.tool.contract.nameserver.NameserverConfigItem;
+import 
org.apache.rocketmq.studio.ops.ai.tool.contract.nameserver.NameserverConfigOutput;
 import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
 import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
 import org.apache.rocketmq.studio.ops.ai.tool.support.PlatformClusterResolver;
@@ -36,7 +36,7 @@ import org.springframework.stereotype.Component;
 @Component
 @RequiredArgsConstructor
 public class NameServerConfigToolHandler
-        implements ToolHandler<NameserverConfigInput, 
ListOutput<NameserverConfigItem>> {
+        implements ToolHandler<NameserverConfigInput, NameserverConfigOutput> {
 
     private final PlatformClusterResolver clusterResolver;
     private final NameServerConfigDiffService configDiffService;
@@ -52,11 +52,13 @@ public class NameServerConfigToolHandler
     }
 
     @Override
-    public ListOutput<NameserverConfigItem> execute(
+    public NameserverConfigOutput execute(
             NameserverConfigInput input, ToolExecutionContext context) {
         String instanceId = 
clusterResolver.resolveInstanceId(input.clusterName());
-        return new ListOutput<>(configDiffService.read(input.clusterName(), 
instanceId).stream()
+        NameServerConfigDiffService.NameServerConfigRead read =
+                configDiffService.read(input.clusterName(), instanceId);
+        return NameserverConfigOutput.of(read.nodes().stream()
                 .map(node -> new NameserverConfigItem(node.addr(), 
node.config()))
-                .toList());
+                .toList(), read.unreachableEndpoints());
     }
 }
diff --git a/server/src/main/resources/tool-catalog/tools/nameserver.yaml 
b/server/src/main/resources/tool-catalog/tools/nameserver.yaml
index 20eff13f6..9c7ae570f 100644
--- a/server/src/main/resources/tool-catalog/tools/nameserver.yaml
+++ b/server/src/main/resources/tool-catalog/tools/nameserver.yaml
@@ -83,10 +83,13 @@ tools:
       type: object
       required:
         - items
+        - resultMayBeTruncated
+        - unreachableEndpoints
       additionalProperties: false
       properties:
         items:
           type: array
+          description: Configuration of the NameServer endpoints that answered.
           items:
             type: object
             required:
@@ -101,5 +104,16 @@ tools:
                   type:
                     - string
                     - 'null'
+        resultMayBeTruncated:
+          type: boolean
+          description: >-
+            True when some NameServer endpoints of the cluster could not be 
read, so items cover
+            only the reachable endpoints. Weigh the partial coverage before 
drawing cluster-wide
+            conclusions, and retry or narrow the scope if needed.
+        unreachableEndpoints:
+          type: array
+          description: Addresses of the NameServer endpoints that could not be 
read.
+          items:
+            type: string
     viewHint: object
     deprecated: false
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffServiceTest.java
index fb9f48256..96d42ec59 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffServiceTest.java
@@ -316,16 +316,16 @@ class NameServerConfigDiffServiceTest {
         when(admin.getNameServerConfig(List.of("ns-b:9876")))
                 .thenReturn(Map.of("ns-b:9876", properties("listenPort", 
"9876")));
 
-        List<NameServerConfigDiffService.NodeConfig> nodes = 
service.read("cluster-a", "instance-a");
+        NameServerConfigDiffService.NameServerConfigRead read = 
service.read("cluster-a", "instance-a");
 
-        assertThat(nodes)
+        assertThat(read.nodes())
                 .extracting(NameServerConfigDiffService.NodeConfig::addr)
                 .containsExactly("ns-a:9876", "ns-b:9876");
-        assertThat(nodes.get(0).config())
+        assertThat(read.nodes().get(0).config())
                 .containsEntry("listenPort", "9876")
                 .containsEntry("serverWorkerThreads", "8")
                 .doesNotContainKey("password");
-        assertThat(nodes.get(1).config()).containsEntry("listenPort", "9876");
+        assertThat(read.nodes().get(1).config()).containsEntry("listenPort", 
"9876");
     }
 
     @Test
@@ -339,9 +339,10 @@ class NameServerConfigDiffServiceTest {
         when(admin.getNameServerConfig(List.of("ns-b:9876")))
                 .thenThrow(new IllegalStateException("unreachable"));
 
-        List<NameServerConfigDiffService.NodeConfig> nodes = 
service.read("cluster-a", null);
+        NameServerConfigDiffService.NameServerConfigRead read = 
service.read("cluster-a", null);
 
-        assertThat(nodes).singleElement()
+        assertThat(read.unreachableEndpoints()).containsExactly("ns-b:9876");
+        assertThat(read.nodes()).singleElement()
                 .extracting(NameServerConfigDiffService.NodeConfig::addr)
                 .isEqualTo("ns-a:9876");
     }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/handler/nameserver/NameServerConfigToolHandlerTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/handler/nameserver/NameServerConfigToolHandlerTest.java
index 93269011e..ca36f7ddc 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/handler/nameserver/NameServerConfigToolHandlerTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/handler/nameserver/NameServerConfigToolHandlerTest.java
@@ -17,9 +17,9 @@
 package org.apache.rocketmq.studio.ops.ai.tool.handler.nameserver;
 
 import 
org.apache.rocketmq.studio.cluster.nameserver.NameServerConfigDiffService;
-import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ListOutput;
 import 
org.apache.rocketmq.studio.ops.ai.tool.contract.nameserver.NameserverConfigInput;
 import 
org.apache.rocketmq.studio.ops.ai.tool.contract.nameserver.NameserverConfigItem;
+import 
org.apache.rocketmq.studio.ops.ai.tool.contract.nameserver.NameserverConfigOutput;
 import org.apache.rocketmq.studio.ops.ai.tool.support.PlatformClusterResolver;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -51,17 +51,38 @@ class NameServerConfigToolHandlerTest {
     void readsConfigThroughResolvedInstanceTest() {
         assertThat(handler.name()).isEqualTo("rmq.nameserver.config");
         
when(clusterResolver.resolveInstanceId("rmq-a")).thenReturn("instance-a");
-        when(configDiffService.read("rmq-a", "instance-a")).thenReturn(List.of(
-                new NameServerConfigDiffService.NodeConfig(
-                        "ns-a:9876", Map.of("listenPort", "9876"))));
+        when(configDiffService.read("rmq-a", "instance-a")).thenReturn(new 
NameServerConfigDiffService.NameServerConfigRead(
+                List.of(new NameServerConfigDiffService.NodeConfig(
+                        "ns-a:9876", Map.of("listenPort", "9876"))),
+                List.of()));
 
-        ListOutput<NameserverConfigItem> result = handler.execute(
+        NameserverConfigOutput result = handler.execute(
                 new NameserverConfigInput("rmq-a"), context("instance-a"));
 
         assertThat(result.items()).singleElement().satisfies(item -> {
             assertThat(item.addr()).isEqualTo("ns-a:9876");
             assertThat(item.config()).containsEntry("listenPort", "9876");
         });
+        assertThat(result.resultMayBeTruncated()).isFalse();
+        assertThat(result.unreachableEndpoints()).isEmpty();
         verify(configDiffService).read("rmq-a", "instance-a");
     }
+
+    @Test
+    void reportsUnreachableEndpointsAsTruncatedCoverageTest() {
+        
when(clusterResolver.resolveInstanceId("rmq-a")).thenReturn("instance-a");
+        when(configDiffService.read("rmq-a", "instance-a")).thenReturn(new 
NameServerConfigDiffService.NameServerConfigRead(
+                List.of(new NameServerConfigDiffService.NodeConfig(
+                        "ns-a:9876", Map.of("listenPort", "9876"))),
+                List.of("ns-b:9876")));
+
+        NameserverConfigOutput result = handler.execute(
+                new NameserverConfigInput("rmq-a"), context("instance-a"));
+
+        assertThat(result.resultMayBeTruncated()).isTrue();
+        assertThat(result.unreachableEndpoints()).containsExactly("ns-b:9876");
+        assertThat(result.items())
+                .extracting(NameserverConfigItem::addr)
+                .containsExactly("ns-a:9876");
+    }
 }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolOutputSchemaContractTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolOutputSchemaContractTest.java
index c99683808..b8dcaed9f 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolOutputSchemaContractTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolOutputSchemaContractTest.java
@@ -52,6 +52,7 @@ import 
org.apache.rocketmq.studio.ops.ai.tool.contract.message.MessageRedelivery
 import 
org.apache.rocketmq.studio.ops.ai.tool.contract.message.MessageSendOutput;
 import 
org.apache.rocketmq.studio.ops.ai.tool.contract.message.MessageTraceOutput;
 import 
org.apache.rocketmq.studio.ops.ai.tool.contract.nameserver.NameserverConfigItem;
+import 
org.apache.rocketmq.studio.ops.ai.tool.contract.nameserver.NameserverConfigOutput;
 import org.apache.rocketmq.studio.ops.ai.tool.contract.ops.AuditItem;
 import org.apache.rocketmq.studio.ops.ai.tool.contract.proxy.ProxyConfigItem;
 import org.apache.rocketmq.studio.ops.ai.tool.contract.topic.TopicDetailOutput;
@@ -226,8 +227,10 @@ class ToolOutputSchemaContractTest {
                 new NameserverListToolHandler.Item(
                         "127.0.0.1:9876", "127.0.0.1:9876", "127.0.0.1:9876",
                         null, null, "UNKNOWN", null)))));
-        samples.put("rmq.nameserver.config", List.of(new ListOutput<>(List.of(
-                new NameserverConfigItem("127.0.0.1:9876", 
Map.of("orderMessageEnable", "false"))))));
+        samples.put("rmq.nameserver.config", List.of(new 
NameserverConfigOutput(
+                List.of(new NameserverConfigItem("127.0.0.1:9876", 
Map.of("orderMessageEnable", "false"))),
+                true,
+                List.of("127.0.0.1:9877"))));
 
         samples.put("rmq.proxy.list", List.of(new 
ListOutput<>(List.of(ProxyVO.builder()
                 .addr("127.0.0.1:8081")

Reply via email to