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

fuyou001 pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/rocketmq.git


The following commit(s) were added to refs/heads/develop by this push:
     new 74e90f7c28 [ISSUE #10446] Support batch deletion of topics and 
subscription groups in broker (#10448)
74e90f7c28 is described below

commit 74e90f7c283d4030e4c89a35f46dec77419bcb9e
Author: Jiahua Wang <[email protected]>
AuthorDate: Mon Jul 13 10:07:43 2026 +0800

    [ISSUE #10446] Support batch deletion of topics and subscription groups in 
broker (#10448)
    
    * [ISSUE #10446] Support batch deletion of topics and subscription groups 
in broker
    
    - Add DELETE_TOPIC_IN_BROKER_LIST and DELETE_SUBSCRIPTION_GROUP_LIST
      request codes with corresponding RequestBody types
    - Implement deleteTopicList / deleteSubscriptionGroupList in
      AdminBrokerProcessor with deduplication and one-shot persist
    - Add deleteTopicConfigList / deleteSubscriptionGroupConfigList in
      TopicConfigManager / SubscriptionGroupManager (single persist per batch)
    - Add MQClientAPIImpl#deleteTopicInBrokerList /
      deleteSubscriptionGroupList client APIs
    - Cover changes with unit tests in AdminBrokerProcessorTest
    
    * fix: persist  config after batch delete
    
    * fix: preserve batch delete failure response
    
    ---------
    
    Co-authored-by: wangjiahua.wjh <[email protected]>
    Co-authored-by: fuchong <[email protected]>
---
 .../DefaultAuthorizationContextBuilder.java        |  35 ++++
 .../DefaultAuthorizationContextBuilderTest.java    |  45 +++++
 .../broker/processor/AdminBrokerProcessor.java     | 202 ++++++++++++++++++---
 .../subscription/SubscriptionGroupManager.java     |  18 +-
 .../rocketmq/broker/topic/TopicConfigManager.java  |  19 +-
 .../broker/processor/AdminBrokerProcessorTest.java | 191 ++++++++++++++++++-
 .../subscription/SubscriptionGroupManagerTest.java |  19 +-
 .../broker/topic/TopicConfigManagerTest.java       |  18 ++
 .../rocketmq/client/impl/MQClientAPIImpl.java      |  46 +++++
 .../rocketmq/client/impl/MQClientAPIImplTest.java  |  62 +++++++
 .../org/apache/rocketmq/common/BrokerConfig.java   |  28 +++
 .../rocketmq/remoting/protocol/RequestCode.java    |   3 +
 .../DeleteSubscriptionGroupListRequestBody.java    |  56 ++++++
 .../protocol/body/DeleteTopicListRequestBody.java  |  41 +++++
 14 files changed, 755 insertions(+), 28 deletions(-)

diff --git 
a/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilder.java
 
b/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilder.java
index f462aabc0d..7f4fca5117 100644
--- 
a/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilder.java
+++ 
b/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilder.java
@@ -68,6 +68,8 @@ import org.apache.rocketmq.remoting.protocol.NamespaceUtil;
 import org.apache.rocketmq.remoting.protocol.RemotingCommand;
 import org.apache.rocketmq.remoting.protocol.RequestCode;
 import org.apache.rocketmq.remoting.protocol.RequestHeaderRegistry;
+import 
org.apache.rocketmq.remoting.protocol.body.DeleteSubscriptionGroupListRequestBody;
+import org.apache.rocketmq.remoting.protocol.body.DeleteTopicListRequestBody;
 import org.apache.rocketmq.remoting.protocol.body.LockBatchRequestBody;
 import org.apache.rocketmq.remoting.protocol.body.UnlockBatchRequestBody;
 import 
org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupRequestHeader;
@@ -322,6 +324,39 @@ public class DefaultAuthorizationContextBuilder implements 
AuthorizationContextB
                         }
                     }
                     break;
+                case RequestCode.DELETE_TOPIC_IN_BROKER_LIST:
+                    // Batch APIs carry their target list in the request body, 
not in an annotated
+                    // CommandCustomHeader, so the annotation-based path in
+                    // RequestHeaderRegistry would otherwise produce an empty 
context list and let
+                    // the request through without a DELETE permission check. 
Decode the body and
+                    // emit one DELETE context per topic instead.
+                    DeleteTopicListRequestBody deleteTopicListRequestBody =
+                        DeleteTopicListRequestBody.decode(command.getBody(), 
DeleteTopicListRequestBody.class);
+                    if 
(CollectionUtils.isNotEmpty(deleteTopicListRequestBody.getTopicList())) {
+                        for (String topicName : 
deleteTopicListRequestBody.getTopicList()) {
+                            if (StringUtils.isBlank(topicName)) {
+                                continue;
+                            }
+                            topic = Resource.ofTopic(topicName);
+                            result.add(DefaultAuthorizationContext.of(subject, 
topic, Action.DELETE, sourceIp));
+                        }
+                    }
+                    break;
+                case RequestCode.DELETE_SUBSCRIPTION_GROUP_LIST:
+                    // See DELETE_TOPIC_IN_BROKER_LIST: emit one DELETE 
context per group from the
+                    // request body so authorization can enforce per-group 
DELETE permission.
+                    DeleteSubscriptionGroupListRequestBody 
deleteGroupListRequestBody =
+                        
DeleteSubscriptionGroupListRequestBody.decode(command.getBody(), 
DeleteSubscriptionGroupListRequestBody.class);
+                    if 
(CollectionUtils.isNotEmpty(deleteGroupListRequestBody.getGroupNameList())) {
+                        for (String groupName : 
deleteGroupListRequestBody.getGroupNameList()) {
+                            if (StringUtils.isBlank(groupName)) {
+                                continue;
+                            }
+                            group = Resource.ofGroup(groupName);
+                            result.add(DefaultAuthorizationContext.of(subject, 
group, Action.DELETE, sourceIp));
+                        }
+                    }
+                    break;
                 default:
                     result = buildContextByAnnotation(subject, command, 
sourceIp);
                     break;
diff --git 
a/auth/src/test/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilderTest.java
 
b/auth/src/test/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilderTest.java
index 920a0e1200..a9de347324 100644
--- 
a/auth/src/test/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilderTest.java
+++ 
b/auth/src/test/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilderTest.java
@@ -57,6 +57,8 @@ import org.apache.rocketmq.remoting.netty.AttributeKeys;
 import org.apache.rocketmq.remoting.protocol.RemotingCommand;
 import org.apache.rocketmq.remoting.protocol.RequestCode;
 import org.apache.rocketmq.remoting.protocol.RequestHeaderRegistry;
+import 
org.apache.rocketmq.remoting.protocol.body.DeleteSubscriptionGroupListRequestBody;
+import org.apache.rocketmq.remoting.protocol.body.DeleteTopicListRequestBody;
 import org.apache.rocketmq.remoting.protocol.body.LockBatchRequestBody;
 import org.apache.rocketmq.remoting.protocol.body.UnlockBatchRequestBody;
 import 
org.apache.rocketmq.remoting.protocol.header.ConsumerSendMsgBackRequestHeader;
@@ -570,6 +572,49 @@ public class DefaultAuthorizationContextBuilderTest {
         Assert.assertEquals("channel-id", getContext(result, 
ResourceType.TOPIC).getChannelId());
         Assert.assertEquals(String.valueOf(RequestCode.UNLOCK_BATCH_MQ), 
getContext(result, ResourceType.TOPIC).getRpcCode());
 
+        // DELETE_TOPIC_IN_BROKER_LIST: body-driven, must yield one DELETE 
context per topic.
+        DeleteTopicListRequestBody deleteTopicListBody = new 
DeleteTopicListRequestBody();
+        deleteTopicListBody.setTopicList(Arrays.asList("topicA", "topicB", "", 
"  "));
+
+        request = 
RemotingCommand.createRequestCommand(RequestCode.DELETE_TOPIC_IN_BROKER_LIST, 
null);
+        request.setBody(JSON.toJSONBytes(deleteTopicListBody));
+        request.setVersion(441);
+        request.addExtField("AccessKey", "rocketmq");
+        request.makeCustomHeaderToNet();
+
+        result = builder.build(channelHandlerContext, request);
+        // Blank entries are filtered, so 2 valid topics produce 2 contexts.
+        Assert.assertEquals(2, result.size());
+        for (DefaultAuthorizationContext ctx : result) {
+            Assert.assertEquals(ResourceType.TOPIC, 
ctx.getResource().getResourceType());
+            Assert.assertEquals("User:rocketmq", 
ctx.getSubject().getSubjectKey());
+            Assert.assertTrue(ctx.getActions().contains(Action.DELETE));
+            
Assert.assertEquals(String.valueOf(RequestCode.DELETE_TOPIC_IN_BROKER_LIST), 
ctx.getRpcCode());
+        }
+        Assert.assertTrue(result.stream().anyMatch(ctx -> 
"Topic:topicA".equals(ctx.getResource().getResourceKey())));
+        Assert.assertTrue(result.stream().anyMatch(ctx -> 
"Topic:topicB".equals(ctx.getResource().getResourceKey())));
+
+        // DELETE_SUBSCRIPTION_GROUP_LIST: body-driven, must yield one DELETE 
context per group.
+        DeleteSubscriptionGroupListRequestBody deleteGroupListBody = new 
DeleteSubscriptionGroupListRequestBody();
+        deleteGroupListBody.setGroupNameList(Arrays.asList("groupX", 
"groupY"));
+
+        request = 
RemotingCommand.createRequestCommand(RequestCode.DELETE_SUBSCRIPTION_GROUP_LIST,
 null);
+        request.setBody(JSON.toJSONBytes(deleteGroupListBody));
+        request.setVersion(441);
+        request.addExtField("AccessKey", "rocketmq");
+        request.makeCustomHeaderToNet();
+
+        result = builder.build(channelHandlerContext, request);
+        Assert.assertEquals(2, result.size());
+        for (DefaultAuthorizationContext ctx : result) {
+            Assert.assertEquals(ResourceType.GROUP, 
ctx.getResource().getResourceType());
+            Assert.assertEquals("User:rocketmq", 
ctx.getSubject().getSubjectKey());
+            Assert.assertTrue(ctx.getActions().contains(Action.DELETE));
+            
Assert.assertEquals(String.valueOf(RequestCode.DELETE_SUBSCRIPTION_GROUP_LIST), 
ctx.getRpcCode());
+        }
+        Assert.assertTrue(result.stream().anyMatch(ctx -> 
"Group:groupX".equals(ctx.getResource().getResourceKey())));
+        Assert.assertTrue(result.stream().anyMatch(ctx -> 
"Group:groupY".equals(ctx.getResource().getResourceKey())));
+
     }
 
     private DefaultAuthorizationContext 
getContext(List<DefaultAuthorizationContext> contexts,
diff --git 
a/broker/src/main/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessor.java
 
b/broker/src/main/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessor.java
index fc0182d733..876a72fa29 100644
--- 
a/broker/src/main/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessor.java
+++ 
b/broker/src/main/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessor.java
@@ -28,9 +28,11 @@ import java.net.UnknownHostException;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -128,6 +130,8 @@ import 
org.apache.rocketmq.remoting.protocol.body.ConsumeQueueData;
 import org.apache.rocketmq.remoting.protocol.body.ConsumeStatsList;
 import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection;
 import org.apache.rocketmq.remoting.protocol.body.CreateTopicListRequestBody;
+import 
org.apache.rocketmq.remoting.protocol.body.DeleteSubscriptionGroupListRequestBody;
+import org.apache.rocketmq.remoting.protocol.body.DeleteTopicListRequestBody;
 import org.apache.rocketmq.remoting.protocol.body.EpochEntryCache;
 import org.apache.rocketmq.remoting.protocol.body.GroupList;
 import org.apache.rocketmq.remoting.protocol.body.HARuntimeInfo;
@@ -270,6 +274,8 @@ public class AdminBrokerProcessor implements 
NettyRequestProcessor {
                 return this.updateAndCreateTopicList(ctx, request);
             case RequestCode.DELETE_TOPIC_IN_BROKER:
                 return this.deleteTopic(ctx, request);
+            case RequestCode.DELETE_TOPIC_IN_BROKER_LIST:
+                return this.deleteTopicList(ctx, request);
             case RequestCode.GET_ALL_TOPIC_CONFIG:
                 return this.getAllTopicConfig(ctx, request);
             case RequestCode.GET_TIMER_CHECK_POINT:
@@ -310,6 +316,8 @@ public class AdminBrokerProcessor implements 
NettyRequestProcessor {
                 return this.getAllSubscriptionGroup(ctx, request);
             case RequestCode.DELETE_SUBSCRIPTIONGROUP:
                 return this.deleteSubscriptionGroup(ctx, request);
+            case RequestCode.DELETE_SUBSCRIPTION_GROUP_LIST:
+                return this.deleteSubscriptionGroupList(ctx, request);
             case RequestCode.GET_TOPIC_STATS_INFO:
                 return this.getTopicStatsInfo(ctx, request);
             case RequestCode.GET_CONSUMER_CONNECTION_LIST:
@@ -782,17 +790,7 @@ public class AdminBrokerProcessor implements 
NettyRequestProcessor {
         topicsToClean.add(topic);
 
         if 
(brokerController.getBrokerConfig().isClearRetryTopicWhenDeleteTopic()) {
-            final Set<String> groups = 
this.brokerController.getConsumerOffsetManager().whichGroupByTopic(topic);
-            for (String group : groups) {
-                final String popRetryTopicV2 = 
KeyBuilder.buildPopRetryTopic(topic, group, true);
-                if 
(brokerController.getTopicConfigManager().selectTopicConfig(popRetryTopicV2) != 
null) {
-                    topicsToClean.add(popRetryTopicV2);
-                }
-                final String popRetryTopicV1 = 
KeyBuilder.buildPopRetryTopicV1(topic, group);
-                if 
(brokerController.getTopicConfigManager().selectTopicConfig(popRetryTopicV1) != 
null) {
-                    topicsToClean.add(popRetryTopicV1);
-                }
-            }
+            collectPopRetryTopics(topic, topicsToClean);
         }
 
         try {
@@ -811,8 +809,96 @@ public class AdminBrokerProcessor implements 
NettyRequestProcessor {
         return response;
     }
 
-    private void deleteTopicInBroker(String topic) {
-        this.brokerController.getTopicConfigManager().deleteTopicConfig(topic);
+    private synchronized RemotingCommand deleteTopicList(ChannelHandlerContext 
ctx,
+        RemotingCommand request) {
+        final RemotingCommand response = 
RemotingCommand.createResponseCommand(null);
+
+        DeleteTopicListRequestBody requestBody = 
DeleteTopicListRequestBody.decode(
+            request.getBody(), DeleteTopicListRequestBody.class);
+        List<String> topicList = requestBody == null ? null : 
requestBody.getTopicList();
+
+        if (CollectionUtils.isEmpty(topicList)) {
+            response.setCode(ResponseCode.INVALID_PARAMETER);
+            response.setRemark("The specified topic list is blank.");
+            return response;
+        }
+
+        LOGGER.info("AdminBrokerProcessor#deleteTopicList: broker receive 
request to delete topics={}, caller={}",
+            topicList, RemotingHelper.parseChannelRemoteAddr(ctx.channel()));
+
+        boolean validateSystemTopic = 
brokerController.getBrokerConfig().isValidateSystemTopicWhenUpdateTopic();
+        // dedup while preserving the input order
+        Set<String> topicsToClean = new LinkedHashSet<>();
+        for (String topic : topicList) {
+            if (UtilAll.isBlank(topic)) {
+                response.setCode(ResponseCode.INVALID_PARAMETER);
+                response.setRemark("The specified topic is blank.");
+                return response;
+            }
+            if (validateSystemTopic && TopicValidator.isSystemTopic(topic)) {
+                response.setCode(ResponseCode.INVALID_PARAMETER);
+                response.setRemark("The topic[" + topic + "] is conflict with 
system topic.");
+                return response;
+            }
+            topicsToClean.add(topic);
+        }
+
+        if 
(brokerController.getBrokerConfig().isClearRetryTopicWhenDeleteTopic()) {
+            // snapshot the inputs before mutating the set, so retry topics 
for already-added retry topics are not collected
+            for (String topic : new ArrayList<>(topicsToClean)) {
+                collectPopRetryTopics(topic, topicsToClean);
+            }
+        }
+
+        boolean isSuccess = false;
+        try {
+            double maxRate = 
this.brokerController.getBrokerConfig().getBatchDeleteTopicMaxRate();
+            com.google.common.util.concurrent.RateLimiter rateLimiter = 
maxRate > 0
+                ? 
com.google.common.util.concurrent.RateLimiter.create(maxRate) : null;
+
+            for (String topic : topicsToClean) {
+                if (rateLimiter != null) {
+                    rateLimiter.acquire();
+                }
+                if (LiteMetadataUtil.isLiteMessageType(topic, 
brokerController)) {
+                    
brokerController.getLiteLifecycleManager().cleanByParentTopic(topic);
+                }
+                deleteTopicInBroker(topic, false);
+            }
+            isSuccess  = true;
+        } catch (Throwable t) {
+            LOGGER.error("Failed to delete topic ", t);
+        } finally {
+            try {
+                this.brokerController.getTopicConfigManager().persist();
+            } catch (Throwable t) {
+                isSuccess = false;
+                LOGGER.error("Failed to persist topic config after batch 
delete", t);
+            }
+        }
+
+        response.setCode(isSuccess ? ResponseCode.SUCCESS : 
ResponseCode.SYSTEM_ERROR);
+        response.setRemark(null);
+
+        return response;
+    }
+
+    private void collectPopRetryTopics(String topic, Collection<String> 
topicsToClean) {
+        final Set<String> groups = 
this.brokerController.getConsumerOffsetManager().whichGroupByTopic(topic);
+        for (String group : groups) {
+            final String popRetryTopicV2 = 
KeyBuilder.buildPopRetryTopic(topic, group, true);
+            if 
(brokerController.getTopicConfigManager().selectTopicConfig(popRetryTopicV2) != 
null) {
+                topicsToClean.add(popRetryTopicV2);
+            }
+            final String popRetryTopicV1 = 
KeyBuilder.buildPopRetryTopicV1(topic, group);
+            if 
(brokerController.getTopicConfigManager().selectTopicConfig(popRetryTopicV1) != 
null) {
+                topicsToClean.add(popRetryTopicV1);
+            }
+        }
+    }
+
+    private void deleteTopicInBroker(String topic, boolean persist) {
+        this.brokerController.getTopicConfigManager().deleteTopicConfig(topic, 
persist);
         this.brokerController.getTopicQueueMappingManager().delete(topic);
         
this.brokerController.getConsumerOffsetManager().cleanOffsetByTopic(topic);
         
this.brokerController.getPopInflightMessageCounter().clearInFlightMessageNumByTopicName(topic);
@@ -820,6 +906,10 @@ public class AdminBrokerProcessor implements 
NettyRequestProcessor {
         
this.brokerController.getMessageStore().getTimerMessageStore().getTimerMetrics().removeTimingCount(topic);
     }
 
+    private void deleteTopicInBroker(String topic) {
+        deleteTopicInBroker(topic, true);
+    }
+
     private RemotingCommand getUnknownCmdResponse(ChannelHandlerContext ctx, 
RemotingCommand request) {
         String error = " request type " + request.getCode() + " not supported";
         final RemotingCommand response =
@@ -1693,18 +1783,88 @@ public class AdminBrokerProcessor implements 
NettyRequestProcessor {
         LOGGER.info("AdminBrokerProcessor#deleteSubscriptionGroup, caller={}",
             RemotingHelper.parseChannelRemoteAddr(ctx.channel()));
 
-        
this.brokerController.getSubscriptionGroupManager().deleteSubscriptionGroupConfig(requestHeader.getGroupName());
+        boolean cleanOffset = requestHeader.isCleanOffset()
+            || LiteMetadataUtil.isLiteGroupType(requestHeader.getGroupName(), 
this.brokerController);
+        deleteSubscriptionGroupInBroker(requestHeader.getGroupName(), 
cleanOffset);
 
-        if (requestHeader.isCleanOffset()
-            || LiteMetadataUtil.isLiteGroupType(requestHeader.getGroupName(), 
this.brokerController)) {
-            
this.brokerController.getConsumerOffsetManager().removeOffset(requestHeader.getGroupName());
-            
this.brokerController.getPopInflightMessageCounter().clearInFlightMessageNumByGroupName(requestHeader.getGroupName());
-        }
+        response.setCode(ResponseCode.SUCCESS);
+        response.setRemark(null);
+        return response;
+    }
 
+    private void deleteSubscriptionGroupInBroker(String groupName, boolean 
cleanOffset) {
+        deleteSubscriptionGroupInBroker(groupName, cleanOffset, true);
+    }
+
+    private void deleteSubscriptionGroupInBroker(String groupName, boolean 
cleanOffset, boolean persist) {
+        
this.brokerController.getSubscriptionGroupManager().deleteSubscriptionGroupConfig(groupName,
 persist);
+        if (cleanOffset) {
+            
this.brokerController.getConsumerOffsetManager().removeOffset(groupName);
+            
this.brokerController.getPopInflightMessageCounter().clearInFlightMessageNumByGroupName(groupName);
+        }
         if (this.brokerController.getBrokerConfig().isAutoDeleteUnusedStats()) 
{
-            
this.brokerController.getBrokerStatsManager().onGroupDeleted(requestHeader.getGroupName());
+            
this.brokerController.getBrokerStatsManager().onGroupDeleted(groupName);
         }
-        response.setCode(ResponseCode.SUCCESS);
+    }
+
+    private RemotingCommand deleteSubscriptionGroupList(ChannelHandlerContext 
ctx,
+        RemotingCommand request) {
+        final RemotingCommand response = 
RemotingCommand.createResponseCommand(null);
+
+        DeleteSubscriptionGroupListRequestBody requestBody = 
DeleteSubscriptionGroupListRequestBody.decode(
+            request.getBody(), DeleteSubscriptionGroupListRequestBody.class);
+        List<String> groupNameList = requestBody == null ? null : 
requestBody.getGroupNameList();
+
+        if (CollectionUtils.isEmpty(groupNameList)) {
+            response.setCode(ResponseCode.INVALID_PARAMETER);
+            response.setRemark("The specified group name list is blank.");
+            return response;
+        }
+
+        // dedup while preserving the input order
+        Set<String> groupNames = new LinkedHashSet<>();
+        for (String groupName : groupNameList) {
+            if (UtilAll.isBlank(groupName)) {
+                response.setCode(ResponseCode.INVALID_PARAMETER);
+                response.setRemark("The specified group name is blank.");
+                return response;
+            }
+            groupNames.add(groupName);
+        }
+
+        LOGGER.info("AdminBrokerProcessor#deleteSubscriptionGroupList: 
groupNames={}, caller={}",
+            groupNames, RemotingHelper.parseChannelRemoteAddr(ctx.channel()));
+
+        boolean isSuccess = false;
+        try {
+            boolean cleanOffset = requestBody.isCleanOffset();
+            double maxRate = 
this.brokerController.getBrokerConfig().getBatchDeleteSubscriptionGroupMaxRate();
+            com.google.common.util.concurrent.RateLimiter rateLimiter = 
maxRate > 0
+                ? 
com.google.common.util.concurrent.RateLimiter.create(maxRate) : null;
+
+            // Check isLiteGroupType before deletion: once the group config is 
removed,
+            // findSubscriptionGroupConfig(...) may auto-recreate the group 
when
+            // autoCreateSubscriptionGroup=true.
+            for (String groupName : groupNames) {
+                if (rateLimiter != null) {
+                    rateLimiter.acquire();
+                }
+                boolean shouldCleanOffset = cleanOffset
+                    || LiteMetadataUtil.isLiteGroupType(groupName, 
this.brokerController);
+                deleteSubscriptionGroupInBroker(groupName, shouldCleanOffset, 
false);
+            }
+            isSuccess = true;
+        } catch (Throwable t) {
+            LOGGER.error("Failed to delete subscription group config ", t);
+        } finally {
+            try {
+                this.brokerController.getSubscriptionGroupManager().persist();
+            } catch (Throwable t) {
+                isSuccess = false;
+                LOGGER.error("Failed to persist subscription group config 
after batch delete", t);
+            }
+        }
+        response.setCode(isSuccess ? ResponseCode.SUCCESS : 
ResponseCode.SYSTEM_ERROR);
         response.setRemark(null);
         return response;
     }
diff --git 
a/broker/src/main/java/org/apache/rocketmq/broker/subscription/SubscriptionGroupManager.java
 
b/broker/src/main/java/org/apache/rocketmq/broker/subscription/SubscriptionGroupManager.java
index 5850309e8c..b37c35051a 100644
--- 
a/broker/src/main/java/org/apache/rocketmq/broker/subscription/SubscriptionGroupManager.java
+++ 
b/broker/src/main/java/org/apache/rocketmq/broker/subscription/SubscriptionGroupManager.java
@@ -384,18 +384,32 @@ public class SubscriptionGroupManager extends 
ConfigManager {
         }
     }
 
-    public void deleteSubscriptionGroupConfig(final String groupName) {
+    public void deleteSubscriptionGroupConfig(final String groupName, boolean 
persist) {
         SubscriptionGroupConfig old = removeSubscriptionGroupConfig(groupName);
         this.forbiddenTable.remove(groupName);
         if (old != null) {
             log.info("delete subscription group OK, subscription group:{}", 
old);
             updateDataVersion();
-            this.persist();
+            if (persist) {
+                this.persist();
+            }
         } else {
             log.warn("delete subscription group failed, subscription 
groupName: {} not exist", groupName);
         }
     }
 
+    public void deleteSubscriptionGroupConfig(final String groupName) {
+        deleteSubscriptionGroupConfig(groupName, true);
+    }
+
+    public void deleteSubscriptionGroupConfigList(final List<String> 
groupNames) {
+        if (groupNames == null || groupNames.isEmpty()) {
+            return;
+        }
+        for (String groupName : groupNames) {
+            deleteSubscriptionGroupConfig(groupName);
+        }
+    }
 
     public void setSubscriptionGroupTable(ConcurrentMap<String, 
SubscriptionGroupConfig> subscriptionGroupTable) {
         this.subscriptionGroupTable = subscriptionGroupTable;
diff --git 
a/broker/src/main/java/org/apache/rocketmq/broker/topic/TopicConfigManager.java 
b/broker/src/main/java/org/apache/rocketmq/broker/topic/TopicConfigManager.java
index 4b82aacb07..82277322cc 100644
--- 
a/broker/src/main/java/org/apache/rocketmq/broker/topic/TopicConfigManager.java
+++ 
b/broker/src/main/java/org/apache/rocketmq/broker/topic/TopicConfigManager.java
@@ -615,17 +615,32 @@ public class TopicConfigManager extends ConfigManager {
         }
     }
 
-    public void deleteTopicConfig(final String topic) {
+    public void deleteTopicConfig(final String topic, boolean persist) {
         TopicConfig old = removeTopicConfig(topic);
         if (old != null) {
             log.info("delete topic config OK, topic: {}", old);
             updateDataVersion();
-            this.persist();
+            if (persist) {
+                this.persist();
+            }
         } else {
             log.warn("delete topic config failed, topic: {} not exists", 
topic);
         }
     }
 
+    public void deleteTopicConfig(final String topic) {
+        deleteTopicConfig(topic, true);
+    }
+
+    public void deleteTopicConfigList(final List<String> topics) {
+        if (topics == null || topics.isEmpty()) {
+            return;
+        }
+        for (String topic : topics) {
+            deleteTopicConfig(topic);
+        }
+    }
+
     public TopicConfigSerializeWrapper buildTopicConfigSerializeWrapper() {
         TopicConfigSerializeWrapper topicConfigSerializeWrapper = new 
TopicConfigSerializeWrapper();
         DataVersion dataVersionCopy = new DataVersion();
diff --git 
a/broker/src/test/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessorTest.java
 
b/broker/src/test/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessorTest.java
index c342400d14..6a32adbe1d 100644
--- 
a/broker/src/test/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessorTest.java
+++ 
b/broker/src/test/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessorTest.java
@@ -44,6 +44,7 @@ import 
org.apache.rocketmq.broker.metrics.BrokerMetricsManager;
 import org.apache.rocketmq.broker.lite.LiteLifecycleManager;
 import org.apache.rocketmq.broker.offset.ConsumerOffsetManager;
 import org.apache.rocketmq.broker.schedule.ScheduleMessageService;
+import org.apache.rocketmq.broker.subscription.SubscriptionGroupManager;
 import org.apache.rocketmq.broker.topic.TopicConfigManager;
 import org.apache.rocketmq.common.BoundaryType;
 import org.apache.rocketmq.common.BrokerConfig;
@@ -76,6 +77,8 @@ import org.apache.rocketmq.remoting.protocol.ResponseCode;
 import org.apache.rocketmq.remoting.protocol.body.AclInfo;
 import 
org.apache.rocketmq.remoting.protocol.body.ConsumerOffsetSerializeWrapper;
 import org.apache.rocketmq.remoting.protocol.body.CreateTopicListRequestBody;
+import 
org.apache.rocketmq.remoting.protocol.body.DeleteSubscriptionGroupListRequestBody;
+import org.apache.rocketmq.remoting.protocol.body.DeleteTopicListRequestBody;
 import org.apache.rocketmq.remoting.protocol.body.GroupList;
 import org.apache.rocketmq.remoting.protocol.body.HARuntimeInfo;
 import org.apache.rocketmq.remoting.protocol.body.LockBatchRequestBody;
@@ -470,11 +473,181 @@ public class AdminBrokerProcessorTest {
         RemotingCommand response = 
adminBrokerProcessor.processRequest(handlerContext, request);
         assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS);
 
-        verify(topicConfigManager).deleteTopicConfig(topic);
-        
verify(topicConfigManager).deleteTopicConfig(KeyBuilder.buildPopRetryTopic(topic,
 "cid1", brokerConfig.isEnableRetryTopicV2()));
+        verify(topicConfigManager).deleteTopicConfig(topic, true);
+        
verify(topicConfigManager).deleteTopicConfig(KeyBuilder.buildPopRetryTopic(topic,
 "cid1", brokerConfig.isEnableRetryTopicV2()), true);
         verify(messageStore, times(2)).deleteTopics(anySet());
     }
 
+    @Test
+    public void testDeleteTopicListInBroker() throws Exception {
+        // empty list should fail with INVALID_PARAMETER
+        RemotingCommand emptyRequest = buildDeleteTopicListRequest(new 
ArrayList<>());
+        RemotingCommand emptyResponse = 
adminBrokerProcessor.processRequest(handlerContext, emptyRequest);
+        
assertThat(emptyResponse.getCode()).isEqualTo(ResponseCode.INVALID_PARAMETER);
+
+        // blank topic name should fail
+        RemotingCommand blankRequest = 
buildDeleteTopicListRequest(Arrays.asList("VALID_TOPIC", "", "ANOTHER"));
+        RemotingCommand blankResponse = 
adminBrokerProcessor.processRequest(handlerContext, blankRequest);
+        
assertThat(blankResponse.getCode()).isEqualTo(ResponseCode.INVALID_PARAMETER);
+        assertThat(blankResponse.getRemark()).isEqualTo("The specified topic 
is blank.");
+
+        // system topic in batch should be rejected
+        for (String sysTopic : systemTopicSet) {
+            RemotingCommand request = 
buildDeleteTopicListRequest(Arrays.asList("TEST_DELETE_TOPIC_BATCH_OK", 
sysTopic));
+            RemotingCommand response = 
adminBrokerProcessor.processRequest(handlerContext, request);
+            
assertThat(response.getCode()).isEqualTo(ResponseCode.INVALID_PARAMETER);
+            assertThat(response.getRemark()).isEqualTo("The topic[" + sysTopic 
+ "] is conflict with system topic.");
+        }
+
+        // happy path
+        List<String> topicList = Arrays.asList("TEST_DELETE_TOPIC_BATCH_1", 
"TEST_DELETE_TOPIC_BATCH_2");
+        RemotingCommand request = buildDeleteTopicListRequest(topicList);
+        RemotingCommand response = 
adminBrokerProcessor.processRequest(handlerContext, request);
+        assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS);
+    }
+
+    @Test
+    public void testDeleteTopicListBatchPersist() throws Exception {
+        // Disable rate limiter for test
+        brokerController.getBrokerConfig().setBatchDeleteTopicMaxRate(0);
+
+        topicConfigManager = mock(TopicConfigManager.class);
+        
when(brokerController.getTopicConfigManager()).thenReturn(topicConfigManager);
+        
when(topicConfigManager.selectTopicConfig(anyString())).thenReturn(null);
+
+        
when(brokerController.getConsumerOffsetManager()).thenReturn(consumerOffsetManager);
+        
when(consumerOffsetManager.whichGroupByTopic(anyString())).thenReturn(new 
HashSet<>());
+
+        List<String> topicList = Arrays.asList("TEST_DELETE_TOPIC_BATCH_A", 
"TEST_DELETE_TOPIC_BATCH_B", "TEST_DELETE_TOPIC_BATCH_A");
+        RemotingCommand request = buildDeleteTopicListRequest(topicList);
+        RemotingCommand response = 
adminBrokerProcessor.processRequest(handlerContext, request);
+        assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS);
+        
verify(topicConfigManager).deleteTopicConfig("TEST_DELETE_TOPIC_BATCH_A", 
false);
+        
verify(topicConfigManager).deleteTopicConfig("TEST_DELETE_TOPIC_BATCH_B", 
false);
+        verify(topicConfigManager).persist();
+    }
+
+    @Test
+    public void testDeleteTopicListRateLimited() throws Exception {
+        // Enable rate limiter at 1000/s so the test exercises the throttled 
path without being slow
+        brokerController.getBrokerConfig().setBatchDeleteTopicMaxRate(1000.0);
+
+        List<String> topicList = Arrays.asList("RL_TOPIC_1", "RL_TOPIC_2", 
"RL_TOPIC_3");
+        RemotingCommand request = buildDeleteTopicListRequest(topicList);
+        RemotingCommand response = 
adminBrokerProcessor.processRequest(handlerContext, request);
+        assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS);
+    }
+
+    @Test
+    public void testDeleteSubscriptionGroupList() throws Exception {
+        // empty list should fail with INVALID_PARAMETER
+        RemotingCommand emptyRequest = 
buildDeleteSubscriptionGroupListRequest(new ArrayList<>(), false);
+        RemotingCommand emptyResponse = 
adminBrokerProcessor.processRequest(handlerContext, emptyRequest);
+        
assertThat(emptyResponse.getCode()).isEqualTo(ResponseCode.INVALID_PARAMETER);
+
+        // blank group name should fail
+        RemotingCommand blankRequest = 
buildDeleteSubscriptionGroupListRequest(Arrays.asList("GID-OK", "", "GID-OK2"), 
true);
+        RemotingCommand blankResponse = 
adminBrokerProcessor.processRequest(handlerContext, blankRequest);
+        
assertThat(blankResponse.getCode()).isEqualTo(ResponseCode.INVALID_PARAMETER);
+
+        // happy path
+        List<String> groupList = Arrays.asList("GID-Group-Name-1", 
"GID-Group-Name-2");
+        RemotingCommand request = 
buildDeleteSubscriptionGroupListRequest(groupList, true);
+        RemotingCommand response = 
adminBrokerProcessor.processRequest(handlerContext, request);
+        assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS);
+    }
+
+    @Test
+    public void testDeleteSubscriptionGroupListDedup() throws Exception {
+        // Duplicate group names should be deduplicated, request should still 
succeed
+        
brokerController.getBrokerConfig().setBatchDeleteSubscriptionGroupMaxRate(0);
+
+        List<String> groupList = Arrays.asList("GID-DUP-1", "GID-DUP-2", 
"GID-DUP-1");
+        RemotingCommand request = 
buildDeleteSubscriptionGroupListRequest(groupList, false);
+        RemotingCommand response = 
adminBrokerProcessor.processRequest(handlerContext, request);
+        assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS);
+    }
+
+    @Test
+    public void testDeleteSubscriptionGroupListBatchPersist() throws Exception 
{
+        
brokerController.getBrokerConfig().setBatchDeleteSubscriptionGroupMaxRate(0);
+        SubscriptionGroupManager subscriptionGroupManager = 
mock(SubscriptionGroupManager.class);
+        brokerController.setSubscriptionGroupManager(subscriptionGroupManager);
+
+        List<String> groupList = Arrays.asList("GID-BATCH-A", "GID-BATCH-B", 
"GID-BATCH-A");
+        RemotingCommand request = 
buildDeleteSubscriptionGroupListRequest(groupList, false);
+        RemotingCommand response = 
adminBrokerProcessor.processRequest(handlerContext, request);
+
+        assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS);
+        
verify(subscriptionGroupManager).deleteSubscriptionGroupConfig("GID-BATCH-A", 
false);
+        
verify(subscriptionGroupManager).deleteSubscriptionGroupConfig("GID-BATCH-B", 
false);
+        verify(subscriptionGroupManager).persist();
+    }
+
+    @Test
+    public void testDeleteSubscriptionGroupListRateLimited() throws Exception {
+        // Enable rate limiter at 1000/s so the test exercises the throttled 
path without being slow
+        
brokerController.getBrokerConfig().setBatchDeleteSubscriptionGroupMaxRate(1000.0);
+
+        List<String> groupList = Arrays.asList("GID-RL-1", "GID-RL-2", 
"GID-RL-3");
+        RemotingCommand request = 
buildDeleteSubscriptionGroupListRequest(groupList, true);
+        RemotingCommand response = 
adminBrokerProcessor.processRequest(handlerContext, request);
+        assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS);
+    }
+
+    @Test
+    public void testDeleteSubscriptionGroupListCleanOffset() throws Exception {
+        // cleanOffset=true should invoke removeOffset for each group
+        
brokerController.getBrokerConfig().setBatchDeleteSubscriptionGroupMaxRate(0);
+
+        List<String> groupList = Arrays.asList("GID-CLEAN-1", "GID-CLEAN-2");
+        RemotingCommand request = 
buildDeleteSubscriptionGroupListRequest(groupList, true);
+        RemotingCommand response = 
adminBrokerProcessor.processRequest(handlerContext, request);
+        assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS);
+    }
+
+    @Test
+    public void testDeleteSubscriptionGroupListNoCleanOffset() throws 
Exception {
+        // cleanOffset=false should still succeed (offset not cleaned unless 
isLiteGroupType)
+        
brokerController.getBrokerConfig().setBatchDeleteSubscriptionGroupMaxRate(0);
+
+        List<String> groupList = Arrays.asList("GID-NOCLEAN-1", 
"GID-NOCLEAN-2");
+        RemotingCommand request = 
buildDeleteSubscriptionGroupListRequest(groupList, false);
+        RemotingCommand response = 
adminBrokerProcessor.processRequest(handlerContext, request);
+        assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS);
+    }
+
+    @Test
+    public void testDeleteTopicListWithPopRetryTopics() throws Exception {
+        // When clearRetryTopicWhenDeleteTopic=true, POP retry topics should 
be collected and deleted
+        brokerController.getBrokerConfig().setBatchDeleteTopicMaxRate(0);
+
+        topicConfigManager = mock(TopicConfigManager.class);
+        
when(brokerController.getTopicConfigManager()).thenReturn(topicConfigManager);
+        String retryTopic = 
org.apache.rocketmq.common.KeyBuilder.buildPopRetryTopic("myTopic", "cid1",
+            brokerController.getBrokerConfig().isEnableRetryTopicV2());
+        when(topicConfigManager.selectTopicConfig(anyString())).thenAnswer(inv 
-> {
+            String t = inv.getArgument(0);
+            if ("myTopic".equals(t) || retryTopic.equals(t)) {
+                return new org.apache.rocketmq.common.TopicConfig();
+            }
+            return null;
+        });
+
+        
when(brokerController.getConsumerOffsetManager()).thenReturn(consumerOffsetManager);
+        
when(consumerOffsetManager.whichGroupByTopic("myTopic")).thenReturn(com.google.common.collect.Sets.newHashSet("cid1"));
+
+        List<String> topicList = Arrays.asList("myTopic");
+        RemotingCommand request = buildDeleteTopicListRequest(topicList);
+        RemotingCommand response = 
adminBrokerProcessor.processRequest(handlerContext, request);
+        assertThat(response.getCode()).isEqualTo(ResponseCode.SUCCESS);
+
+        // Verify both the original topic and the retry topic were deleted
+        
org.mockito.Mockito.verify(topicConfigManager).deleteTopicConfig("myTopic", 
false);
+        
org.mockito.Mockito.verify(topicConfigManager).deleteTopicConfig(retryTopic, 
false);
+        org.mockito.Mockito.verify(topicConfigManager).persist();
+    }
+
     @Test
     public void testGetAllTopicConfigInRocksdb() throws Exception {
         if (notToBeExecuted()) {
@@ -1691,6 +1864,20 @@ public class AdminBrokerProcessorTest {
         return request;
     }
 
+    private RemotingCommand buildDeleteTopicListRequest(List<String> 
topicList) {
+        DeleteTopicListRequestBody requestBody = new 
DeleteTopicListRequestBody(topicList);
+        RemotingCommand request = 
RemotingCommand.createRequestCommand(RequestCode.DELETE_TOPIC_IN_BROKER_LIST, 
null);
+        request.setBody(requestBody.encode());
+        return request;
+    }
+
+    private RemotingCommand 
buildDeleteSubscriptionGroupListRequest(List<String> groupNameList, boolean 
cleanOffset) {
+        DeleteSubscriptionGroupListRequestBody requestBody = new 
DeleteSubscriptionGroupListRequestBody(groupNameList, cleanOffset);
+        RemotingCommand request = 
RemotingCommand.createRequestCommand(RequestCode.DELETE_SUBSCRIPTION_GROUP_LIST,
 null);
+        request.setBody(requestBody.encode());
+        return request;
+    }
+
     private MessageExt createDefaultMessageExt() {
         MessageExt messageExt = new MessageExt();
         messageExt.setMsgId("12345678");
diff --git 
a/broker/src/test/java/org/apache/rocketmq/broker/subscription/SubscriptionGroupManagerTest.java
 
b/broker/src/test/java/org/apache/rocketmq/broker/subscription/SubscriptionGroupManagerTest.java
index bc34e26bf5..9f709f6502 100644
--- 
a/broker/src/test/java/org/apache/rocketmq/broker/subscription/SubscriptionGroupManagerTest.java
+++ 
b/broker/src/test/java/org/apache/rocketmq/broker/subscription/SubscriptionGroupManagerTest.java
@@ -163,6 +163,23 @@ public class SubscriptionGroupManagerTest {
             
assertThat(subscriptionGroupManager.getSubscriptionGroupTable().get(groupName)).isNotNull());
     }
 
+    @Test
+    public void testDeleteSubscriptionGroupConfigWithDeferredPersist() {
+        Mockito.doNothing().when(subscriptionGroupManager).persist();
+
+        SubscriptionGroupConfig deferredGroup = new SubscriptionGroupConfig();
+        deferredGroup.setGroupName("deferred-group");
+        
subscriptionGroupManager.getSubscriptionGroupTable().put(deferredGroup.getGroupName(),
 deferredGroup);
+        
subscriptionGroupManager.deleteSubscriptionGroupConfig(deferredGroup.getGroupName(),
 false);
+        verify(subscriptionGroupManager, never()).persist();
+
+        SubscriptionGroupConfig persistedGroup = new SubscriptionGroupConfig();
+        persistedGroup.setGroupName("persisted-group");
+        
subscriptionGroupManager.getSubscriptionGroupTable().put(persistedGroup.getGroupName(),
 persistedGroup);
+        
subscriptionGroupManager.deleteSubscriptionGroupConfig(persistedGroup.getGroupName(),
 true);
+        verify(subscriptionGroupManager).persist();
+    }
+
     @Test
     public void testSubGroupTable() {
         // Empty SubscriptionGroupManager
@@ -234,4 +251,4 @@ public class SubscriptionGroupManagerTest {
         }
     }
 
-}
\ No newline at end of file
+}
diff --git 
a/broker/src/test/java/org/apache/rocketmq/broker/topic/TopicConfigManagerTest.java
 
b/broker/src/test/java/org/apache/rocketmq/broker/topic/TopicConfigManagerTest.java
index 085c3f9cfb..fd91b5c0eb 100644
--- 
a/broker/src/test/java/org/apache/rocketmq/broker/topic/TopicConfigManagerTest.java
+++ 
b/broker/src/test/java/org/apache/rocketmq/broker/topic/TopicConfigManagerTest.java
@@ -140,6 +140,24 @@ public class TopicConfigManagerTest {
         Assert.assertEquals("only add attribute is supported while creating 
topic. key: " + key, runtimeException.getMessage());
     }
 
+    @Test
+    public void testDeleteTopicConfigWithDeferredPersist() {
+        TopicConfigManager manager = Mockito.spy(topicConfigManager);
+        Mockito.doNothing().when(manager).persist();
+
+        TopicConfig deferredTopic = new TopicConfig();
+        deferredTopic.setTopicName("deferred-topic");
+        manager.getTopicConfigTable().put(deferredTopic.getTopicName(), 
deferredTopic);
+        manager.deleteTopicConfig(deferredTopic.getTopicName(), false);
+        Mockito.verify(manager, Mockito.never()).persist();
+
+        TopicConfig persistedTopic = new TopicConfig();
+        persistedTopic.setTopicName("persisted-topic");
+        manager.getTopicConfigTable().put(persistedTopic.getTopicName(), 
persistedTopic);
+        manager.deleteTopicConfig(persistedTopic.getTopicName(), true);
+        Mockito.verify(manager).persist();
+    }
+
     @Test
     public void testAddWrongValueOnCreating() {
         Map<String, String> attributes = new HashMap<>();
diff --git 
a/client/src/main/java/org/apache/rocketmq/client/impl/MQClientAPIImpl.java 
b/client/src/main/java/org/apache/rocketmq/client/impl/MQClientAPIImpl.java
index 8294ffd422..6999859491 100644
--- a/client/src/main/java/org/apache/rocketmq/client/impl/MQClientAPIImpl.java
+++ b/client/src/main/java/org/apache/rocketmq/client/impl/MQClientAPIImpl.java
@@ -125,6 +125,8 @@ import 
org.apache.rocketmq.remoting.protocol.body.ConsumeStatsList;
 import org.apache.rocketmq.remoting.protocol.body.ConsumerConnection;
 import org.apache.rocketmq.remoting.protocol.body.ConsumerRunningInfo;
 import org.apache.rocketmq.remoting.protocol.body.CreateTopicListRequestBody;
+import 
org.apache.rocketmq.remoting.protocol.body.DeleteSubscriptionGroupListRequestBody;
+import org.apache.rocketmq.remoting.protocol.body.DeleteTopicListRequestBody;
 import org.apache.rocketmq.remoting.protocol.body.EpochEntryCache;
 import 
org.apache.rocketmq.remoting.protocol.body.GetBrokerLiteInfoResponseBody;
 import org.apache.rocketmq.remoting.protocol.body.GetConsumerStatusBody;
@@ -2197,6 +2199,27 @@ public class MQClientAPIImpl implements 
NameServerUpdateCallback, StartAndShutdo
         throw new MQClientException(response.getCode(), response.getRemark());
     }
 
+    public void deleteTopicInBrokerList(final String addr, final List<String> 
topicList, final long timeoutMillis)
+        throws RemotingException, InterruptedException, MQClientException {
+        DeleteTopicListRequestBody requestBody = new 
DeleteTopicListRequestBody(topicList);
+        RemotingCommand request = 
RemotingCommand.createRequestCommand(RequestCode.DELETE_TOPIC_IN_BROKER_LIST, 
null);
+        request.setBody(requestBody.encode());
+        RemotingCommand response = 
this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(),
 addr),
+            request, timeoutMillis);
+        if (response == null) {
+            throw new MQClientException(ResponseCode.SYSTEM_ERROR, "response 
is null when deleting topic list in broker");
+        }
+        switch (response.getCode()) {
+            case ResponseCode.SUCCESS: {
+                return;
+            }
+            default:
+                break;
+        }
+
+        throw new MQClientException(response.getCode(), response.getRemark());
+    }
+
     public void deleteTopicInNameServer(final String addr, final String topic, 
final long timeoutMillis)
         throws RemotingException, InterruptedException, MQClientException {
         DeleteTopicFromNamesrvRequestHeader requestHeader = new 
DeleteTopicFromNamesrvRequestHeader();
@@ -2257,6 +2280,29 @@ public class MQClientAPIImpl implements 
NameServerUpdateCallback, StartAndShutdo
         throw new MQClientException(response.getCode(), response.getRemark());
     }
 
+    public void deleteSubscriptionGroupList(final String addr, final 
List<String> groupNameList, final boolean cleanOffset,
+        final long timeoutMillis)
+        throws RemotingException, InterruptedException, MQClientException {
+        DeleteSubscriptionGroupListRequestBody requestBody = new 
DeleteSubscriptionGroupListRequestBody(groupNameList, cleanOffset);
+        RemotingCommand request = 
RemotingCommand.createRequestCommand(RequestCode.DELETE_SUBSCRIPTION_GROUP_LIST,
 null);
+        request.setBody(requestBody.encode());
+
+        RemotingCommand response = 
this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(),
 addr),
+            request, timeoutMillis);
+        if (response == null) {
+            throw new MQClientException(ResponseCode.SYSTEM_ERROR, "response 
is null when deleting subscription group list");
+        }
+        switch (response.getCode()) {
+            case ResponseCode.SUCCESS: {
+                return;
+            }
+            default:
+                break;
+        }
+
+        throw new MQClientException(response.getCode(), response.getRemark());
+    }
+
     public String getKVConfigValue(final String namespace, final String key, 
final long timeoutMillis)
         throws RemotingException, MQClientException, InterruptedException {
         GetKVConfigRequestHeader requestHeader = new 
GetKVConfigRequestHeader();
diff --git 
a/client/src/test/java/org/apache/rocketmq/client/impl/MQClientAPIImplTest.java 
b/client/src/test/java/org/apache/rocketmq/client/impl/MQClientAPIImplTest.java
index 27b3d68571..096853c6e6 100644
--- 
a/client/src/test/java/org/apache/rocketmq/client/impl/MQClientAPIImplTest.java
+++ 
b/client/src/test/java/org/apache/rocketmq/client/impl/MQClientAPIImplTest.java
@@ -1364,6 +1364,68 @@ public class MQClientAPIImplTest {
         mqClientAPI.deleteSubscriptionGroup(defaultBrokerAddr, "", true, 
defaultTimeout);
     }
 
+    @Test
+    public void testDeleteTopicInBrokerList() throws RemotingException, 
InterruptedException, MQClientException {
+        mockInvokeSync();
+        mqClientAPI.deleteTopicInBrokerList(defaultBrokerAddr, 
java.util.Arrays.asList("topicA", "topicB"), defaultTimeout);
+    }
+
+    @Test(expected = MQClientException.class)
+    public void testDeleteTopicInBrokerListFail() throws RemotingException, 
InterruptedException, MQClientException {
+        when(response.getCode()).thenReturn(ResponseCode.SYSTEM_ERROR);
+        when(response.getRemark()).thenReturn("error");
+        when(remotingClient.invokeSync(any(), any(), 
anyLong())).thenReturn(response);
+        mqClientAPI.deleteTopicInBrokerList(defaultBrokerAddr, 
java.util.Arrays.asList("topicA"), defaultTimeout);
+    }
+
+    @Test(expected = MQClientException.class)
+    public void testDeleteTopicInBrokerListNullResponse() throws 
RemotingException, InterruptedException, MQClientException {
+        when(remotingClient.invokeSync(any(), any(), 
anyLong())).thenReturn(null);
+        mqClientAPI.deleteTopicInBrokerList(defaultBrokerAddr, 
java.util.Arrays.asList("topicA"), defaultTimeout);
+    }
+
+    @Test
+    public void testDeleteTopicInBrokerListVerifyRequest() throws Exception {
+        mockInvokeSync();
+        org.mockito.ArgumentCaptor<RemotingCommand> captor = 
org.mockito.ArgumentCaptor.forClass(RemotingCommand.class);
+        mqClientAPI.deleteTopicInBrokerList(defaultBrokerAddr, 
java.util.Arrays.asList("t1", "t2"), defaultTimeout);
+        org.mockito.Mockito.verify(remotingClient).invokeSync(any(), 
captor.capture(), anyLong());
+        RemotingCommand captured = captor.getValue();
+        assertEquals(RequestCode.DELETE_TOPIC_IN_BROKER_LIST, 
captured.getCode());
+        assertNotNull(captured.getBody());
+    }
+
+    @Test
+    public void testDeleteSubscriptionGroupList() throws RemotingException, 
InterruptedException, MQClientException {
+        mockInvokeSync();
+        mqClientAPI.deleteSubscriptionGroupList(defaultBrokerAddr, 
java.util.Arrays.asList("groupA", "groupB"), true, defaultTimeout);
+    }
+
+    @Test(expected = MQClientException.class)
+    public void testDeleteSubscriptionGroupListFail() throws 
RemotingException, InterruptedException, MQClientException {
+        when(response.getCode()).thenReturn(ResponseCode.SYSTEM_ERROR);
+        when(response.getRemark()).thenReturn("error");
+        when(remotingClient.invokeSync(any(), any(), 
anyLong())).thenReturn(response);
+        mqClientAPI.deleteSubscriptionGroupList(defaultBrokerAddr, 
java.util.Arrays.asList("groupA"), false, defaultTimeout);
+    }
+
+    @Test(expected = MQClientException.class)
+    public void testDeleteSubscriptionGroupListNullResponse() throws 
RemotingException, InterruptedException, MQClientException {
+        when(remotingClient.invokeSync(any(), any(), 
anyLong())).thenReturn(null);
+        mqClientAPI.deleteSubscriptionGroupList(defaultBrokerAddr, 
java.util.Arrays.asList("groupA"), true, defaultTimeout);
+    }
+
+    @Test
+    public void testDeleteSubscriptionGroupListVerifyRequest() throws 
Exception {
+        mockInvokeSync();
+        org.mockito.ArgumentCaptor<RemotingCommand> captor = 
org.mockito.ArgumentCaptor.forClass(RemotingCommand.class);
+        mqClientAPI.deleteSubscriptionGroupList(defaultBrokerAddr, 
java.util.Arrays.asList("g1", "g2"), true, defaultTimeout);
+        org.mockito.Mockito.verify(remotingClient).invokeSync(any(), 
captor.capture(), anyLong());
+        RemotingCommand captured = captor.getValue();
+        assertEquals(RequestCode.DELETE_SUBSCRIPTION_GROUP_LIST, 
captured.getCode());
+        assertNotNull(captured.getBody());
+    }
+
     @Test
     public void assertGetKVConfigValue() throws RemotingException, 
InterruptedException, MQClientException {
         mockInvokeSync();
diff --git a/common/src/main/java/org/apache/rocketmq/common/BrokerConfig.java 
b/common/src/main/java/org/apache/rocketmq/common/BrokerConfig.java
index 3c0ba3f6ca..3ab78fba5f 100644
--- a/common/src/main/java/org/apache/rocketmq/common/BrokerConfig.java
+++ b/common/src/main/java/org/apache/rocketmq/common/BrokerConfig.java
@@ -379,6 +379,18 @@ public class BrokerConfig extends BrokerIdentity {
 
     private boolean validateSystemTopicWhenUpdateTopic = true;
 
+    /**
+     * Maximum rate (permits per second) for batch topic deletion.
+     * Setting to 0 or negative disables rate limiting.
+     */
+    private double batchDeleteTopicMaxRate = 10.0;
+
+    /**
+     * Maximum rate (permits per second) for batch subscription-group deletion.
+     * Setting to 0 or negative disables rate limiting.
+     */
+    private double batchDeleteSubscriptionGroupMaxRate = 10.0;
+
     /**
      * It is an important basis for the controller to choose the broker master.
      * The lower the value of brokerElectionPriority, the higher the priority 
of the broker being selected as the master.
@@ -2062,6 +2074,22 @@ public class BrokerConfig extends BrokerIdentity {
         this.validateSystemTopicWhenUpdateTopic = 
validateSystemTopicWhenUpdateTopic;
     }
 
+    public double getBatchDeleteTopicMaxRate() {
+        return batchDeleteTopicMaxRate;
+    }
+
+    public void setBatchDeleteTopicMaxRate(double batchDeleteTopicMaxRate) {
+        this.batchDeleteTopicMaxRate = batchDeleteTopicMaxRate;
+    }
+
+    public double getBatchDeleteSubscriptionGroupMaxRate() {
+        return batchDeleteSubscriptionGroupMaxRate;
+    }
+
+    public void setBatchDeleteSubscriptionGroupMaxRate(double 
batchDeleteSubscriptionGroupMaxRate) {
+        this.batchDeleteSubscriptionGroupMaxRate = 
batchDeleteSubscriptionGroupMaxRate;
+    }
+
     public boolean isEstimateAccumulation() {
         return estimateAccumulation;
     }
diff --git 
a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RequestCode.java 
b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RequestCode.java
index b32dbbc87e..32530905e4 100644
--- 
a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RequestCode.java
+++ 
b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/RequestCode.java
@@ -311,4 +311,7 @@ public class RequestCode {
     public static final int AUTH_LIST_ACL = 3010;
 
     public static final int SWITCH_TIMER_ENGINE = 5001;
+
+    public static final int DELETE_TOPIC_IN_BROKER_LIST = 5002;
+    public static final int DELETE_SUBSCRIPTION_GROUP_LIST = 5003;
 }
diff --git 
a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/DeleteSubscriptionGroupListRequestBody.java
 
b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/DeleteSubscriptionGroupListRequestBody.java
new file mode 100644
index 0000000000..7c78fb69af
--- /dev/null
+++ 
b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/DeleteSubscriptionGroupListRequestBody.java
@@ -0,0 +1,56 @@
+/*
+ * 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.remoting.protocol.body;
+
+import java.util.List;
+import org.apache.rocketmq.remoting.annotation.CFNotNull;
+import org.apache.rocketmq.remoting.protocol.RemotingSerializable;
+
+public class DeleteSubscriptionGroupListRequestBody extends 
RemotingSerializable {
+    @CFNotNull
+    private List<String> groupNameList;
+
+    private boolean cleanOffset = false;
+
+    public DeleteSubscriptionGroupListRequestBody() {
+    }
+
+    public DeleteSubscriptionGroupListRequestBody(List<String> groupNameList) {
+        this.groupNameList = groupNameList;
+    }
+
+    public DeleteSubscriptionGroupListRequestBody(List<String> groupNameList, 
boolean cleanOffset) {
+        this.groupNameList = groupNameList;
+        this.cleanOffset = cleanOffset;
+    }
+
+    public List<String> getGroupNameList() {
+        return groupNameList;
+    }
+
+    public void setGroupNameList(List<String> groupNameList) {
+        this.groupNameList = groupNameList;
+    }
+
+    public boolean isCleanOffset() {
+        return cleanOffset;
+    }
+
+    public void setCleanOffset(boolean cleanOffset) {
+        this.cleanOffset = cleanOffset;
+    }
+}
diff --git 
a/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/DeleteTopicListRequestBody.java
 
b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/DeleteTopicListRequestBody.java
new file mode 100644
index 0000000000..8677e3b338
--- /dev/null
+++ 
b/remoting/src/main/java/org/apache/rocketmq/remoting/protocol/body/DeleteTopicListRequestBody.java
@@ -0,0 +1,41 @@
+/*
+ * 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.remoting.protocol.body;
+
+import java.util.List;
+import org.apache.rocketmq.remoting.annotation.CFNotNull;
+import org.apache.rocketmq.remoting.protocol.RemotingSerializable;
+
+public class DeleteTopicListRequestBody extends RemotingSerializable {
+    @CFNotNull
+    private List<String> topicList;
+
+    public DeleteTopicListRequestBody() {
+    }
+
+    public DeleteTopicListRequestBody(List<String> topicList) {
+        this.topicList = topicList;
+    }
+
+    public List<String> getTopicList() {
+        return topicList;
+    }
+
+    public void setTopicList(List<String> topicList) {
+        this.topicList = topicList;
+    }
+}

Reply via email to