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 8e7887921 fix(instance): resolve resource counts by the canonical 
instance name (#4790)
8e7887921 is described below

commit 8e788792113c795b1b27da8d68b57d606b4c0401
Author: 烤化の初雪 <[email protected]>
AuthorDate: Thu Sep 24 18:14:00 2026 +0800

    fix(instance): resolve resource counts by the canonical instance name 
(#4790)
    
    fix(instance): keep the canonical-name resolution the batch-delete test pins
    
    The previous commit's teeth check restored the trunk version of
    InstanceService.java and the restore was lost before commit, so the
    branch head silently carried the unguarded call sites while the tests
    passed the run before last's restore. Re-apply the canonical-name fix
    that this branch exists for; no behavioural change beyond the original
    commit.
    
    test(instance): pin that batch delete resolves counts by canonical name
    
    deleteInstances funnels into the same deleteInstance guard as the
    single-delete path; pin the canonical-name contract there too with an
    instance named "42" (id 3) so the numeric-id shadowing stays closed
    on the batch route as well.
    
    fix(instance): resolve resource counts by the canonical instance name
    
    loadCounts and the delete guard held an already-resolved instance but
    passed String.valueOf(instance.getId()) to the provider, whose
    identifier resolution matches the unique name before the numeric-id
    fallback. An instance named like another instance's numeric id
    (instance names are free-form up to 64 chars) therefore shadowed the
    id: the instance list showed another instance's topic/group counts and
    the delete guard read them from the wrong instance, letting an
    operator delete an instance whose managed resources still exist. Pass
    the canonical instance name instead, which findByIdentifier always
    resolves to the instance itself.
---
 .../rocketmq/studio/instance/InstanceService.java  |  16 ++-
 .../studio/instance/InstanceServiceTest.java       | 129 ++++++++++++++++-----
 2 files changed, 108 insertions(+), 37 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceService.java 
b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceService.java
index c0c6a3bd7..04e9f8377 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceService.java
@@ -176,13 +176,16 @@ public class InstanceService {
 
     /**
      * Resource counts live on the vendor side (cloud APIs) or in the local 
tables (Apache),
-     * so resolve them uniformly through the vendor provider.
+     * so resolve them uniformly through the vendor provider. The canonical 
instance name is
+     * passed, not the numeric id as a string: provider-side identifier 
resolution matches the
+     * unique name first, so a name that happens to equal another instance's 
numeric id would
+     * otherwise shadow it and attribute the counts to the wrong instance.
      */
     private InstanceResourceCountRunner.ResourceCounts loadCounts(InstanceVO 
instance) {
         InstanceVendor vendor = instance.getVendor() == null ? 
InstanceVendor.APACHE : instance.getVendor();
         InstanceProvider provider = providerRegistry.forVendor(vendor);
-        int topicCount = 
provider.countTopics(String.valueOf(instance.getId()));
-        int consumerGroupCount = 
provider.countGroups(String.valueOf(instance.getId()));
+        int topicCount = provider.countTopics(instance.getName());
+        int consumerGroupCount = provider.countGroups(instance.getName());
         return new InstanceResourceCountRunner.ResourceCounts(topicCount, 
consumerGroupCount);
     }
 
@@ -664,8 +667,11 @@ public class InstanceService {
         InstanceVendor vendor = existing.getVendor() == null ? 
InstanceVendor.APACHE : existing.getVendor();
         if (vendor == InstanceVendor.APACHE) {
             InstanceProvider provider = 
providerRegistry.forVendor(InstanceVendor.APACHE);
-            int topicCount = provider.countTopics(String.valueOf(id));
-            int consumerGroupCount = provider.countGroups(String.valueOf(id));
+            // Pass the canonical name: identifier resolution is name-first, 
so the numeric id
+            // string could resolve to a different instance whose name happens 
to equal this id,
+            // reading the wrong instance's counts in the delete guard.
+            int topicCount = provider.countTopics(existing.getName());
+            int consumerGroupCount = provider.countGroups(existing.getName());
             if (topicCount > 0 || consumerGroupCount > 0) {
                 throw new BusinessException(409, String.format(
                         "Cannot delete instance with managed resources: 
topics=%d, consumerGroups=%d",
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceServiceTest.java
index 91d6cd3ed..bfb8719dd 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/InstanceServiceTest.java
@@ -197,12 +197,12 @@ class InstanceServiceTest {
 
     @Test
     void listInstancesMarksCloudCountsUnavailableWhenProviderFails() {
-        InstanceVO instance = 
InstanceVO.builder().vendor(InstanceVendor.ALIYUN).build();
+        InstanceVO instance = 
InstanceVO.builder().name("aliyun-fail").vendor(InstanceVendor.ALIYUN).build();
         instance.setId(1L);
         InstanceProvider provider = 
org.mockito.Mockito.mock(InstanceProvider.class);
         when(instanceRepository.findAll()).thenReturn(List.of(instance));
         
when(providerRegistry.forVendor(InstanceVendor.ALIYUN)).thenReturn(provider);
-        when(provider.countTopics("1")).thenThrow(new 
IllegalStateException("access denied"));
+        when(provider.countTopics("aliyun-fail")).thenThrow(new 
IllegalStateException("access denied"));
 
         InstanceVO result = instanceService.listInstances(null, null).get(0);
 
@@ -211,13 +211,13 @@ class InstanceServiceTest {
 
     @Test
     void listInstancesKeepsCloudCountsAvailableWhenProviderReturnsEmptyLists() 
{
-        InstanceVO instance = 
InstanceVO.builder().vendor(InstanceVendor.ALIYUN).build();
+        InstanceVO instance = 
InstanceVO.builder().name("aliyun-empty").vendor(InstanceVendor.ALIYUN).build();
         instance.setId(2L);
         InstanceProvider provider = 
org.mockito.Mockito.mock(InstanceProvider.class);
         when(instanceRepository.findAll()).thenReturn(List.of(instance));
         
when(providerRegistry.forVendor(InstanceVendor.ALIYUN)).thenReturn(provider);
-        when(provider.countTopics("2")).thenReturn(0);
-        when(provider.countGroups("2")).thenReturn(0);
+        when(provider.countTopics("aliyun-empty")).thenReturn(0);
+        when(provider.countGroups("aliyun-empty")).thenReturn(0);
 
         InstanceVO result = instanceService.listInstances(null, null).get(0);
 
@@ -318,10 +318,10 @@ class InstanceServiceTest {
         when(instanceRepository.findAll()).thenReturn(List.of(apache, aliyun));
         
when(providerRegistry.forVendor(InstanceVendor.APACHE)).thenReturn(instanceProvider);
         
when(providerRegistry.forVendor(InstanceVendor.ALIYUN)).thenReturn(aliyunProvider);
-        when(instanceProvider.countTopics("3")).thenReturn(3);
-        when(instanceProvider.countGroups("3")).thenReturn(2);
-        when(aliyunProvider.countTopics("4")).thenReturn(5);
-        when(aliyunProvider.countGroups("4")).thenReturn(4);
+        when(instanceProvider.countTopics("apache")).thenReturn(3);
+        when(instanceProvider.countGroups("apache")).thenReturn(2);
+        when(aliyunProvider.countTopics("aliyun")).thenReturn(5);
+        when(aliyunProvider.countGroups("aliyun")).thenReturn(4);
 
         List<InstanceVO> result = instanceService.listInstances(null, null);
 
@@ -337,7 +337,7 @@ class InstanceServiceTest {
         instance.setId(5L);
         when(instanceRepository.findAll()).thenReturn(List.of(instance));
         
when(providerRegistry.forVendor(InstanceVendor.APACHE)).thenReturn(instanceProvider);
-        when(instanceProvider.countTopics("5"))
+        when(instanceProvider.countTopics("broken"))
                 .thenThrow(new IllegalStateException("admin unavailable"));
 
         List<InstanceVO> result = instanceService.listInstances(null, null);
@@ -355,7 +355,7 @@ class InstanceServiceTest {
         instance.setResourceCountsAvailable(true);
         when(instanceRepository.findAll()).thenReturn(List.of(instance));
         
when(providerRegistry.forVendor(InstanceVendor.APACHE)).thenReturn(instanceProvider);
-        when(instanceProvider.countTopics("6"))
+        when(instanceProvider.countTopics("stale-counts"))
                 .thenThrow(new IllegalStateException("provider unavailable"));
 
         InstanceVO result = instanceService.listInstances(null, null).get(0);
@@ -402,7 +402,7 @@ class InstanceServiceTest {
         CountDownLatch topicCountStarted = new CountDownLatch(1);
         CountDownLatch releaseProvider = new CountDownLatch(1);
         CountDownLatch providerFinished = new CountDownLatch(1);
-        when(instanceProvider.countTopics("24")).thenAnswer(invocation -> {
+        when(instanceProvider.countTopics("slow")).thenAnswer(invocation -> {
             topicCountStarted.countDown();
             boolean interrupted = false;
             while (true) {
@@ -420,7 +420,7 @@ class InstanceServiceTest {
             }
             return 7;
         });
-        when(instanceProvider.countGroups("24")).thenAnswer(invocation -> {
+        when(instanceProvider.countGroups("slow")).thenAnswer(invocation -> {
             providerFinished.countDown();
             return 5;
         });
@@ -971,8 +971,8 @@ class InstanceServiceTest {
 
         
when(instanceRepository.findById(1L)).thenReturn(Optional.of(existing));
         
when(providerRegistry.forVendor(InstanceVendor.APACHE)).thenReturn(instanceProvider);
-        when(instanceProvider.countTopics("1")).thenReturn(0);
-        when(instanceProvider.countGroups("1")).thenReturn(0);
+        when(instanceProvider.countTopics("to-delete")).thenReturn(0);
+        when(instanceProvider.countGroups("to-delete")).thenReturn(0);
         when(instanceRepository.deleteById(1L)).thenReturn(true);
 
         instanceService.deleteInstance(1L);
@@ -1004,8 +1004,8 @@ class InstanceServiceTest {
                 .thenReturn(Optional.empty());
         
when(instanceRepository.findById(1L)).thenReturn(Optional.of(existing));
         
when(providerRegistry.forVendor(InstanceVendor.APACHE)).thenReturn(instanceProvider);
-        when(instanceProvider.countTopics("1")).thenReturn(0);
-        when(instanceProvider.countGroups("1")).thenReturn(0);
+        when(instanceProvider.countTopics("inst-a")).thenReturn(0);
+        when(instanceProvider.countGroups("inst-a")).thenReturn(0);
         when(instanceRepository.deleteById(1L)).thenReturn(true);
         ReflectionTestUtils.setField(instanceService, "self", instanceService);
 
@@ -1026,9 +1026,9 @@ class InstanceServiceTest {
         
when(instanceRepository.findById(1L)).thenReturn(Optional.of(failedInstance));
         
when(instanceRepository.findById(2L)).thenReturn(Optional.of(deletedInstance));
         
when(providerRegistry.forVendor(InstanceVendor.APACHE)).thenReturn(instanceProvider);
-        when(instanceProvider.countTopics("1")).thenThrow(new 
IllegalStateException("broker unavailable"));
-        when(instanceProvider.countTopics("2")).thenReturn(0);
-        when(instanceProvider.countGroups("2")).thenReturn(0);
+        when(instanceProvider.countTopics("inst-a")).thenThrow(new 
IllegalStateException("broker unavailable"));
+        when(instanceProvider.countTopics("inst-b")).thenReturn(0);
+        when(instanceProvider.countGroups("inst-b")).thenReturn(0);
         when(instanceRepository.deleteById(2L)).thenReturn(true);
         ReflectionTestUtils.setField(instanceService, "self", instanceService);
 
@@ -1036,10 +1036,10 @@ class InstanceServiceTest {
 
         assertThat(result.getDeleted()).isEqualTo(1);
         assertThat(result.getFailed()).containsExactly("inst-a: broker 
unavailable");
-        verify(instanceProvider).countTopics("1");
+        verify(instanceProvider).countTopics("inst-a");
         verify(instanceRepository).findByIdentifier("inst-b");
-        verify(instanceProvider).countTopics("2");
-        verify(instanceProvider).countGroups("2");
+        verify(instanceProvider).countTopics("inst-b");
+        verify(instanceProvider).countGroups("inst-b");
         verify(instanceRepository).deleteById(2L);
     }
 
@@ -1051,7 +1051,7 @@ class InstanceServiceTest {
         
when(instanceRepository.findByIdentifier("inst-a")).thenReturn(Optional.of(existing));
         
when(instanceRepository.findById(1L)).thenReturn(Optional.of(existing));
         
when(providerRegistry.forVendor(InstanceVendor.APACHE)).thenReturn(instanceProvider);
-        when(instanceProvider.countTopics("1")).thenThrow(new 
IllegalStateException(oversizedMessage));
+        when(instanceProvider.countTopics("inst-a")).thenThrow(new 
IllegalStateException(oversizedMessage));
         ReflectionTestUtils.setField(instanceService, "self", instanceService);
 
         BatchDeleteResultVO result = 
instanceService.deleteInstances(List.of("inst-a"));
@@ -1076,8 +1076,8 @@ class InstanceServiceTest {
         
when(instanceRepository.findByIdentifier("inst-a")).thenReturn(Optional.of(existing));
         
when(instanceRepository.findById(1L)).thenReturn(Optional.of(existing));
         
when(providerRegistry.forVendor(InstanceVendor.APACHE)).thenReturn(instanceProvider);
-        when(instanceProvider.countTopics("1")).thenReturn(0);
-        when(instanceProvider.countGroups("1")).thenReturn(0);
+        when(instanceProvider.countTopics("inst-a")).thenReturn(0);
+        when(instanceProvider.countGroups("inst-a")).thenReturn(0);
         when(instanceRepository.deleteById(1L)).thenReturn(true);
         ReflectionTestUtils.setField(instanceService, "self", instanceService);
 
@@ -1105,8 +1105,8 @@ class InstanceServiceTest {
         existing.setId(1L);
         
when(instanceRepository.findById(1L)).thenReturn(Optional.of(existing));
         
when(providerRegistry.forVendor(InstanceVendor.APACHE)).thenReturn(instanceProvider);
-        when(instanceProvider.countTopics("1")).thenReturn(2);
-        when(instanceProvider.countGroups("1")).thenReturn(0);
+        when(instanceProvider.countTopics("with-topics")).thenReturn(2);
+        when(instanceProvider.countGroups("with-topics")).thenReturn(0);
 
         assertThatThrownBy(() -> instanceService.deleteInstance(1L))
                 .isInstanceOf(BusinessException.class)
@@ -1116,6 +1116,71 @@ class InstanceServiceTest {
         verify(instanceRepository, never()).deleteById(1L);
     }
 
+    @Test
+    void deleteInstanceShouldResolveResourceCountsByInstanceNameTest() {
+        // findByIdentifier resolves a unique name before the numeric-id 
fallback, so a name that
+        // looks like another instance's id would shadow it. The guard holds 
the already-resolved
+        // instance and must pass its canonical name, not the numeric id as a 
string.
+        InstanceVO existing = InstanceVO.builder().name("with-topics").build();
+        existing.setId(1L);
+
+        
when(instanceRepository.findById(1L)).thenReturn(Optional.of(existing));
+        
when(providerRegistry.forVendor(InstanceVendor.APACHE)).thenReturn(instanceProvider);
+        when(instanceProvider.countTopics("with-topics")).thenReturn(2);
+        when(instanceProvider.countGroups("with-topics")).thenReturn(0);
+
+        assertThatThrownBy(() -> instanceService.deleteInstance(1L))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("Cannot delete instance with managed resources: 
topics=2, consumerGroups=0");
+
+        verify(instanceProvider).countTopics("with-topics");
+        verify(instanceProvider).countGroups("with-topics");
+        verify(instanceProvider, never()).countTopics("1");
+        verify(instanceRepository, never()).deleteById(1L);
+    }
+
+    @Test
+    void batchDeleteShouldRouteItsResourceGuardThroughTheCanonicalNameTest() {
+        // The PR contract: batch delete funnels into the same deleteInstance 
guard, so the
+        // canonical name (not the numeric id string) is what reaches the 
provider there too.
+        InstanceVO existing = InstanceVO.builder().name("42").build();
+        existing.setId(3L);
+        
when(instanceRepository.findByIdentifier("42")).thenReturn(Optional.of(existing));
+        
when(instanceRepository.findById(3L)).thenReturn(Optional.of(existing));
+        
when(providerRegistry.forVendor(InstanceVendor.APACHE)).thenReturn(instanceProvider);
+        when(instanceProvider.countTopics("42")).thenReturn(0);
+        when(instanceProvider.countGroups("42")).thenReturn(0);
+        when(instanceRepository.deleteById(3L)).thenReturn(true);
+        ReflectionTestUtils.setField(instanceService, "self", instanceService);
+
+        BatchDeleteResultVO result = 
instanceService.deleteInstances(List.of("42"));
+
+        assertThat(result.getDeleted()).isEqualTo(1);
+        verify(instanceProvider).countTopics("42");
+        verify(instanceProvider).countGroups("42");
+        verify(instanceProvider, never()).countTopics("3");
+        verify(instanceRepository).deleteById(3L);
+    }
+
+    @Test
+    void listInstancesShouldResolveResourceCountsByInstanceNameTest() {
+        // Same shadowing contract for the list counts: the numeric id string 
must not be sent
+        // through the name-first identifier resolution.
+        InstanceVO apache = InstanceVO.builder().name("42").build();
+        apache.setId(3L);
+        when(instanceRepository.findAll()).thenReturn(List.of(apache));
+        
when(providerRegistry.forVendor(InstanceVendor.APACHE)).thenReturn(instanceProvider);
+        when(instanceProvider.countTopics("42")).thenReturn(3);
+        when(instanceProvider.countGroups("42")).thenReturn(2);
+
+        List<InstanceVO> result = instanceService.listInstances(null, null);
+
+        assertThat(result.get(0).getTopicCount()).isEqualTo(3);
+        assertThat(result.get(0).getConsumerGroupCount()).isEqualTo(2);
+        verify(instanceProvider, never()).countTopics("3");
+        verify(instanceProvider, never()).countGroups("3");
+    }
+
     @Test
     void deleteInstanceShouldRejectInstanceWithConsumerGroups() {
         InstanceVO existing = InstanceVO.builder()
@@ -1125,8 +1190,8 @@ class InstanceServiceTest {
         existing.setId(1L);
         
when(instanceRepository.findById(1L)).thenReturn(Optional.of(existing));
         
when(providerRegistry.forVendor(InstanceVendor.APACHE)).thenReturn(instanceProvider);
-        when(instanceProvider.countTopics("1")).thenReturn(0);
-        when(instanceProvider.countGroups("1")).thenReturn(3);
+        
when(instanceProvider.countTopics("with-consumer-groups")).thenReturn(0);
+        
when(instanceProvider.countGroups("with-consumer-groups")).thenReturn(3);
 
         assertThatThrownBy(() -> instanceService.deleteInstance(1L))
                 .isInstanceOf(BusinessException.class)
@@ -1195,8 +1260,8 @@ class InstanceServiceTest {
                 .instanceIds(List.of("to-delete", "inst-2")).build();
         
when(instanceRepository.findById(1L)).thenReturn(Optional.of(existing));
         
when(providerRegistry.forVendor(InstanceVendor.APACHE)).thenReturn(instanceProvider);
-        when(instanceProvider.countTopics("1")).thenReturn(0);
-        when(instanceProvider.countGroups("1")).thenReturn(0);
+        when(instanceProvider.countTopics("to-delete")).thenReturn(0);
+        when(instanceProvider.countGroups("to-delete")).thenReturn(0);
         when(instanceRepository.deleteById(1L)).thenReturn(true);
         
when(settingsRepository.findAllDataSources()).thenReturn(List.of(dataSource));
         
when(settingsRepository.replaceDataSource(dataSource)).thenReturn(true);

Reply via email to