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 05682d759 fix(group): apply or reject the consume model of a cloud 
consumer group (#6548)
05682d759 is described below

commit 05682d759ebea1ec25f7ced457b3a1e8cd5c613c
Author: Apulupie <[email protected]>
AuthorDate: Thu Oct 15 18:15:17 2026 +0800

    fix(group): apply or reject the consume model of a cloud consumer group 
(#6548)
    
    The group-create contract carries ConsumerGroupVO.consumeType, and the tool 
and REST responses
    echo the submitted VO, but neither cloud adapter applied the requested 
model.
    
    AliyunInstanceProvider.createConsumerGroup never set messageModel even 
though the OpenAPI
    request has the field, so a broadcast request created a clustering group; 
the group page reads
    the real model back through 
AliyunConverters.toConsumeType(data.getMessageModel()), so the
    downgrade surfaced later as an unexplained change. The request now carries 
the vendor's own
    spelling for a requested broadcast, while an unspecified or clustering 
model keeps the field out
    of the payload exactly as before.
    
    TencentInstanceProvider.createConsumerGroup cannot apply it at all: 
CreateConsumerGroupRequest
    has no consume-model field (only instanceId, consumerGroup, maxRetryTimes, 
consumeEnable,
    consumeMessageOrderly, remark, tagList, retryPolicy, liteTopic), while
    enrichConsumerGroupDetail reports DescribeConsumerGroup's real 
consumeModel. validateConsumerGroup
    now rejects a broadcast request with 400 before any SDK call, which is what
    InstanceProvider#updateConsumerGroup's contract asks for - never report a 
change the adapter
    cannot apply - and both adapters still apply the fields their APIs do 
accept (maxRetryTimes,
    consumeMessageOrderly, deliveryOrderType).
---
 .../studio/provider/alibaba/AliyunConverters.java  | 10 +++++
 .../provider/alibaba/AliyunInstanceProvider.java   | 13 ++++--
 .../provider/tencent/TencentInstanceProvider.java  |  9 ++++
 .../alibaba/AliyunInstanceProviderTest.java        | 50 ++++++++++++++++++++++
 .../tencent/TencentInstanceProviderTest.java       | 33 ++++++++++++++
 5 files changed, 112 insertions(+), 3 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunConverters.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunConverters.java
index 708bea0b9..a5eded738 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunConverters.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunConverters.java
@@ -196,6 +196,16 @@ final class AliyunConverters {
         return ConsumeType.CLUSTERING;
     }
 
+    /**
+     * The inverse of {@link #toConsumeType}: the OpenAPI's own spelling for a 
requested consume model
+     * ({@code Broadcasting} / {@code Clustering} — the values the read paths 
recognise). Null for an
+     * unspecified or clustering model, so a request that does not ask for 
broadcast keeps the field
+     * out of the payload and the vendor default in charge.
+     */
+    static String toMessageModel(ConsumeType consumeType) {
+        return consumeType == ConsumeType.BROADCASTING ? "Broadcasting" : null;
+    }
+
     static List<QueueProgressVO> 
toQueueProgressRows(GetConsumerGroupLagResponseBody.Data data) {
         List<QueueProgressVO> rows = new ArrayList<>();
         Map<String, DataTopicLagMapValue> topicLagMap = data.getTopicLagMap();
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
index a23f302b9..58db22f71 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
@@ -382,12 +382,19 @@ public class AliyunInstanceProvider implements 
InstanceProvider {
         } else {
             retryPolicy.retryPolicy(DEFAULT_RETRY_POLICY);
         }
-        CreateConsumerGroupRequest request = 
CreateConsumerGroupRequest.builder()
+        CreateConsumerGroupRequest.Builder requestBuilder = 
CreateConsumerGroupRequest.builder()
                 .instanceId(ctx.cloudInstanceId())
                 .consumerGroupId(group.getName())
                 .deliveryOrderType(deliveryOrderType)
-                .consumeRetryPolicy(retryPolicy.build())
-                .build();
+                .consumeRetryPolicy(retryPolicy.build());
+        // A requested broadcast model used to be dropped here while the 
response echoed it back and
+        // the group page read the vendor's real (clustering) model afterwards 
- the same silent
+        // downgrade #4816 fixed for the delivery order type.
+        String messageModel = 
AliyunConverters.toMessageModel(group.getConsumeType());
+        if (messageModel != null) {
+            requestBuilder.messageModel(messageModel);
+        }
+        CreateConsumerGroupRequest request = requestBuilder.build();
         clientFactory.call(ctx.credentialId(), ctx.regionId(), client -> 
client.createConsumerGroup(request));
         group.setInstanceId(instanceId);
         group.setDeliveryOrderType(deliveryOrderType);
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
index 4d2628c8c..fcd93cd3b 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
@@ -1083,6 +1083,15 @@ public class TencentInstanceProvider implements 
InstanceProvider {
         if (group == null || !StringUtils.hasText(group.getName())) {
             throw new BusinessException(400, "Consumer group name is 
required");
         }
+        // CreateConsumerGroupRequest has no consume-model field: the OpenAPI 
always creates a
+        // clustering group, so a requested broadcast model cannot be applied. 
Answer that instead of
+        // echoing the request back and letting DescribeConsumerGroup report 
the vendor's real model
+        // later - InstanceProvider#updateConsumerGroup's contract asks a 
provider never to report a
+        // change it cannot apply.
+        if (group.getConsumeType() == ConsumeType.BROADCASTING) {
+            throw new BusinessException(400, "Tencent consumer groups always 
consume in clustering mode; "
+                    + "consumeType BROADCASTING cannot be applied");
+        }
     }
 
     private static void requireGroupName(String groupName) {
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProviderTest.java
index 3e9dcad1d..88b3551d9 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProviderTest.java
@@ -814,6 +814,56 @@ class AliyunInstanceProviderTest {
         assertThat(created.getDeliveryOrderType()).isEqualTo("Orderly");
     }
 
+    @Test
+    void createConsumerGroupShouldApplyTheRequestedBroadcastingModelTest() {
+        stubInstance();
+        stubCallThrough();
+        when(asyncClient.createConsumerGroup(any()))
+                
.thenReturn(CompletableFuture.completedFuture(CreateConsumerGroupResponse.create()
+                        .toBuilder()
+                        .statusCode(200)
+                        
.body(CreateConsumerGroupResponseBody.builder().data(true).build())
+                        .build()));
+        ConsumerGroupVO group = new ConsumerGroupVO();
+        group.setName("GID_broadcast");
+        // what the AI tool's group-create input and any API caller can send
+        group.setConsumeType(ConsumeType.BROADCASTING);
+        group.setRetryMaxTimes(5);
+
+        ConsumerGroupVO created = 
provider.createConsumerGroup(STUDIO_INSTANCE_ID, group);
+
+        ArgumentCaptor<CreateConsumerGroupRequest> captor =
+                ArgumentCaptor.forClass(CreateConsumerGroupRequest.class);
+        verify(asyncClient).createConsumerGroup(captor.capture());
+        // The vendor's spelling, the one the group page reads back through
+        // AliyunConverters.toConsumeType(data.getMessageModel()).
+        
assertThat(captor.getValue().getMessageModel()).isEqualTo("Broadcasting");
+        
assertThat(created.getConsumeType()).isEqualTo(ConsumeType.BROADCASTING);
+    }
+
+    @Test
+    void 
createConsumerGroupShouldLeaveTheConsumeModelToTheVendorWhenClusteringTest() {
+        stubInstance();
+        stubCallThrough();
+        when(asyncClient.createConsumerGroup(any()))
+                
.thenReturn(CompletableFuture.completedFuture(CreateConsumerGroupResponse.create()
+                        .toBuilder()
+                        .statusCode(200)
+                        
.body(CreateConsumerGroupResponseBody.builder().data(true).build())
+                        .build()));
+        ConsumerGroupVO group = new ConsumerGroupVO();
+        group.setName("GID_clustered");
+        group.setConsumeType(ConsumeType.CLUSTERING);
+
+        provider.createConsumerGroup(STUDIO_INSTANCE_ID, group);
+
+        ArgumentCaptor<CreateConsumerGroupRequest> captor =
+                ArgumentCaptor.forClass(CreateConsumerGroupRequest.class);
+        verify(asyncClient).createConsumerGroup(captor.capture());
+        // Only a requested broadcast is sent: the clustering default keeps 
the request byte-identical.
+        assertThat(captor.getValue().getMessageModel()).isNull();
+    }
+
     @Test
     void resetOffsetShouldUseSpecifiedTimeTest() {
         stubInstance();
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
index 7710327c7..fb22003aa 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
@@ -712,6 +712,39 @@ class TencentInstanceProviderTest {
         assertThat(created.getRetryMaxTimes()).isEqualTo(20);
     }
 
+    @Test
+    void 
createConsumerGroupShouldRejectABroadcastingModelTheOpenApiCannotApplyTest() 
throws Exception {
+        ConsumerGroupVO group = new ConsumerGroupVO();
+        group.setName("GID_broadcast");
+        // what the AI tool's group-create input and any API caller can send
+        group.setConsumeType(ConsumeType.BROADCASTING);
+
+        assertThatThrownBy(() -> 
provider.createConsumerGroup(STUDIO_INSTANCE_ID, group))
+                .isInstanceOfSatisfying(BusinessException.class, error -> {
+                    assertThat(error.getCode()).isEqualTo(400);
+                    assertThat(error.getMessage()).contains("clustering");
+                });
+        // CreateConsumerGroupRequest has no consume-model field, so nothing 
may be sent.
+        verifyNoInteractions(client);
+    }
+
+    @Test
+    void createConsumerGroupShouldStillApplyTheOrderedAndRetrySettingsTest() 
throws Exception {
+        when(client.CreateConsumerGroup(any())).thenReturn(null);
+        ConsumerGroupVO group = new ConsumerGroupVO();
+        group.setName("GID_ordered");
+        group.setDeliveryOrderType("MESSAGES_ORDER");
+        group.setRetryMaxTimes(20);
+
+        provider.createConsumerGroup(STUDIO_INSTANCE_ID, group);
+
+        ArgumentCaptor<CreateConsumerGroupRequest> captor = 
ArgumentCaptor.forClass(CreateConsumerGroupRequest.class);
+        verify(client).CreateConsumerGroup(captor.capture());
+        assertThat(captor.getValue().getConsumeMessageOrderly()).isTrue();
+        assertThat(captor.getValue().getMaxRetryTimes()).isEqualTo(20L);
+        assertThat(captor.getValue().getConsumeEnable()).isTrue();
+    }
+
     @Test
     void deleteConsumerGroupShouldCallTencentOpenApiTest() throws Exception {
         when(client.DeleteConsumerGroup(any())).thenReturn(null);

Reply via email to