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

lollipopjin 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 8045955432 [ISSUE #11089] Share consumption executors for Proxy 
internal clients (#11090)
8045955432 is described below

commit 8045955432df0482587ed5cfa58e5c2b219d2729
Author: qianye <[email protected]>
AuthorDate: Fri Sep 11 10:18:26 2026 +0800

    [ISSUE #11089] Share consumption executors for Proxy internal clients 
(#11090)
---
 .../client/consumer/DefaultMQPushConsumer.java     |  34 ++++
 .../consumer/AbstractConsumeMessageService.java    |  81 ++++++++
 .../ConsumeMessageConcurrentlyService.java         |  50 +----
 .../consumer/ConsumeMessageOrderlyService.java     |  48 +----
 .../ConsumeMessagePopConcurrentlyService.java      |  46 +----
 .../consumer/ConsumeMessagePopOrderlyService.java  |  45 +----
 .../impl/consumer/ConsumeMessageService.java       |   1 +
 .../ConsumeMessageExecutorInjectionTest.java       | 203 +++++++++++++++++++++
 .../ConsumeMessagePopConcurrentlyServiceTest.java  |   6 +-
 .../ConsumeMessagePopOrderlyServiceTest.java       |   4 +-
 .../apache/rocketmq/proxy/config/ProxyConfig.java  |   9 +
 .../proxy/service/ClusterServiceManager.java       |   7 +-
 .../service/client/ClusterConsumerManager.java     |  10 +-
 .../sysmessage/AbstractSystemMessageSyncer.java    |   9 +
 .../proxy/service/sysmessage/HeartbeatSyncer.java  |   9 +-
 .../sysmessage/SystemMessageConsumeExecutor.java   |  38 ++++
 .../SystemMessageConsumeExecutorTest.java          |  95 ++++++++++
 .../SystemMessageConsumerSharingTest.java          |  64 +++++++
 18 files changed, 578 insertions(+), 181 deletions(-)

diff --git 
a/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultMQPushConsumer.java
 
b/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultMQPushConsumer.java
index 5df5cc8fa1..9aef301f50 100644
--- 
a/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultMQPushConsumer.java
+++ 
b/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultMQPushConsumer.java
@@ -47,6 +47,7 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
+import java.util.concurrent.ExecutorService;
 
 /**
  * In most scenarios, this is the mostly recommended class to consume messages.
@@ -160,6 +161,8 @@ public class DefaultMQPushConsumer extends ClientConfig 
implements MQPushConsume
      */
     private int consumeThreadMin = 20;
 
+    private ExecutorService consumeExecutor;
+
     /**
      * Max consumer thread number
      */
@@ -558,6 +561,37 @@ public class DefaultMQPushConsumer extends ClientConfig 
implements MQPushConsume
         this.consumerGroup = consumerGroup;
     }
 
+    /**
+     * Returns the externally managed consumption executor, or null for a 
dedicated pool.
+     */
+    public ExecutorService getConsumeExecutor() {
+        return consumeExecutor;
+    }
+
+    /**
+     * Sets an externally managed executor before starting this consumer.
+     *
+     * <p>This is an advanced API intended for controlled integrations such as 
Proxy. Ordinary
+     * applications should use the default consumption pool instead of 
injecting an executor.
+     * The executor may be shared with other consumers. Virtual-thread 
executors are also supported
+     * when supplied by applications running on a compatible JDK.
+     *
+     * <p>While consumers are running, the external executor must avoid 
capacity-based rejection
+     * and must not discard or cancel pending consumption tasks. The client 
does not guarantee
+     * automatic recovery from rejected tasks. Discarding or cancelling tasks 
can retain cached
+     * messages and pin consumption offsets, eventually stalling consumption.
+     *
+     * <p>The caller controls concurrency and owns the executor's lifecycle. 
This consumer never
+     * shuts down or resizes an external executor. Consumer shutdown does not 
await or cancel tasks
+     * submitted to it; the caller must stop all consumers using the executor 
before shutting it
+     * down and awaiting its termination.
+     *
+     * @param consumeExecutor external executor, or null to use the default 
dedicated pool
+     */
+    public void setConsumeExecutor(ExecutorService consumeExecutor) {
+        this.consumeExecutor = consumeExecutor;
+    }
+
     public int getConsumeThreadMax() {
         return consumeThreadMax;
     }
diff --git 
a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/AbstractConsumeMessageService.java
 
b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/AbstractConsumeMessageService.java
new file mode 100644
index 0000000000..27887989bc
--- /dev/null
+++ 
b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/AbstractConsumeMessageService.java
@@ -0,0 +1,81 @@
+/*
+ * 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.client.impl.consumer;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
+import org.apache.rocketmq.common.utils.ThreadUtils;
+
+public abstract class AbstractConsumeMessageService implements 
ConsumeMessageService {
+    protected final DefaultMQPushConsumer defaultMQPushConsumer;
+    protected final ExecutorService consumeExecutor;
+    private final boolean ownsConsumeExecutor;
+
+    protected AbstractConsumeMessageService(DefaultMQPushConsumer 
defaultMQPushConsumer, ThreadFactory threadFactory) {
+        this.defaultMQPushConsumer = defaultMQPushConsumer;
+        ExecutorService externalExecutor = 
defaultMQPushConsumer.getConsumeExecutor();
+        this.ownsConsumeExecutor = externalExecutor == null;
+        if (this.ownsConsumeExecutor) {
+            this.consumeExecutor = new ThreadPoolExecutor(
+                defaultMQPushConsumer.getConsumeThreadMin(),
+                defaultMQPushConsumer.getConsumeThreadMax(),
+                1000 * 60,
+                TimeUnit.MILLISECONDS,
+                new LinkedBlockingQueue<>(),
+                threadFactory);
+        } else {
+            this.consumeExecutor = externalExecutor;
+        }
+    }
+
+    protected static String getConsumerGroupTag(String consumerGroup) {
+        return (consumerGroup.length() > 100 ? consumerGroup.substring(0, 100) 
: consumerGroup) + "_";
+    }
+
+    protected void shutdownConsumeExecutor(long awaitTerminateMillis) {
+        if (this.ownsConsumeExecutor) {
+            ThreadUtils.shutdownGracefully(this.consumeExecutor, 
awaitTerminateMillis, TimeUnit.MILLISECONDS);
+        }
+    }
+
+    @Override
+    public void updateCorePoolSize(int corePoolSize) {
+        if (this.ownsConsumeExecutor
+            && corePoolSize > 0
+            && corePoolSize <= Short.MAX_VALUE
+            && corePoolSize < 
this.defaultMQPushConsumer.getConsumeThreadMax()) {
+            ((ThreadPoolExecutor) 
this.consumeExecutor).setCorePoolSize(corePoolSize);
+        }
+    }
+
+    @Override
+    public void incCorePoolSize() {
+    }
+
+    @Override
+    public void decCorePoolSize() {
+    }
+
+    @Override
+    public int getCorePoolSize() {
+        return this.ownsConsumeExecutor ? ((ThreadPoolExecutor) 
this.consumeExecutor).getCorePoolSize() : -1;
+    }
+}
diff --git 
a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageConcurrentlyService.java
 
b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageConcurrentlyService.java
index b151fefbbb..361da434ff 100644
--- 
a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageConcurrentlyService.java
+++ 
b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageConcurrentlyService.java
@@ -22,14 +22,10 @@ import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.Executors;
-import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
-import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
 import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
 import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
 import org.apache.rocketmq.client.consumer.listener.ConsumeReturnType;
@@ -42,19 +38,15 @@ import org.apache.rocketmq.common.UtilAll;
 import org.apache.rocketmq.common.message.MessageAccessor;
 import org.apache.rocketmq.common.message.MessageExt;
 import org.apache.rocketmq.common.message.MessageQueue;
-import org.apache.rocketmq.common.utils.ThreadUtils;
 import org.apache.rocketmq.remoting.protocol.body.CMResult;
 import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult;
 import org.apache.rocketmq.logging.org.slf4j.Logger;
 import org.apache.rocketmq.logging.org.slf4j.LoggerFactory;
 
-public class ConsumeMessageConcurrentlyService implements 
ConsumeMessageService {
+public class ConsumeMessageConcurrentlyService extends 
AbstractConsumeMessageService {
     private static final Logger log = 
LoggerFactory.getLogger(ConsumeMessageConcurrentlyService.class);
     private final DefaultMQPushConsumerImpl defaultMQPushConsumerImpl;
-    private final DefaultMQPushConsumer defaultMQPushConsumer;
     private final MessageListenerConcurrently messageListener;
-    private final BlockingQueue<Runnable> consumeRequestQueue;
-    private final ThreadPoolExecutor consumeExecutor;
     private final String consumerGroup;
 
     private final ScheduledExecutorService scheduledExecutorService;
@@ -62,22 +54,14 @@ public class ConsumeMessageConcurrentlyService implements 
ConsumeMessageService
 
     public ConsumeMessageConcurrentlyService(DefaultMQPushConsumerImpl 
defaultMQPushConsumerImpl,
         MessageListenerConcurrently messageListener) {
+        super(defaultMQPushConsumerImpl.getDefaultMQPushConsumer(), new 
ThreadFactoryImpl("ConsumeMessageThread_"
+            + 
getConsumerGroupTag(defaultMQPushConsumerImpl.getDefaultMQPushConsumer().getConsumerGroup())));
         this.defaultMQPushConsumerImpl = defaultMQPushConsumerImpl;
         this.messageListener = messageListener;
 
-        this.defaultMQPushConsumer = 
this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer();
         this.consumerGroup = this.defaultMQPushConsumer.getConsumerGroup();
-        this.consumeRequestQueue = new LinkedBlockingQueue<>();
-
-        String consumerGroupTag = (consumerGroup.length() > 100 ? 
consumerGroup.substring(0, 100) : consumerGroup) + "_";
-        this.consumeExecutor = new ThreadPoolExecutor(
-            this.defaultMQPushConsumer.getConsumeThreadMin(),
-            this.defaultMQPushConsumer.getConsumeThreadMax(),
-            1000 * 60,
-            TimeUnit.MILLISECONDS,
-            this.consumeRequestQueue,
-            new ThreadFactoryImpl("ConsumeMessageThread_" + consumerGroupTag));
 
+        String consumerGroupTag = getConsumerGroupTag(consumerGroup);
         this.scheduledExecutorService = 
Executors.newSingleThreadScheduledExecutor(new 
ThreadFactoryImpl("ConsumeMessageScheduledThread_" + consumerGroupTag));
         this.cleanExpireMsgExecutors = 
Executors.newSingleThreadScheduledExecutor(new 
ThreadFactoryImpl("CleanExpireMsgScheduledThread_" + consumerGroupTag));
     }
@@ -99,34 +83,10 @@ public class ConsumeMessageConcurrentlyService implements 
ConsumeMessageService
 
     public void shutdown(long awaitTerminateMillis) {
         this.scheduledExecutorService.shutdown();
-        ThreadUtils.shutdownGracefully(this.consumeExecutor, 
awaitTerminateMillis, TimeUnit.MILLISECONDS);
+        shutdownConsumeExecutor(awaitTerminateMillis);
         this.cleanExpireMsgExecutors.shutdown();
     }
 
-    @Override
-    public void updateCorePoolSize(int corePoolSize) {
-        if (corePoolSize > 0
-            && corePoolSize <= Short.MAX_VALUE
-            && corePoolSize < 
this.defaultMQPushConsumer.getConsumeThreadMax()) {
-            this.consumeExecutor.setCorePoolSize(corePoolSize);
-        }
-    }
-
-    @Override
-    public void incCorePoolSize() {
-
-    }
-
-    @Override
-    public void decCorePoolSize() {
-
-    }
-
-    @Override
-    public int getCorePoolSize() {
-        return this.consumeExecutor.getCorePoolSize();
-    }
-
     @Override
     public ConsumeMessageDirectlyResult consumeMessageDirectly(MessageExt msg, 
String brokerName) {
         ConsumeMessageDirectlyResult result = new 
ConsumeMessageDirectlyResult();
diff --git 
a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageOrderlyService.java
 
b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageOrderlyService.java
index 3ca465da70..776eb129c7 100644
--- 
a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageOrderlyService.java
+++ 
b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageOrderlyService.java
@@ -20,14 +20,10 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
-import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.Executors;
-import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import org.apache.commons.lang3.StringUtils;
-import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
 import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext;
 import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
 import org.apache.rocketmq.client.consumer.listener.ConsumeReturnType;
@@ -42,7 +38,6 @@ import org.apache.rocketmq.common.message.MessageAccessor;
 import org.apache.rocketmq.common.message.MessageConst;
 import org.apache.rocketmq.common.message.MessageExt;
 import org.apache.rocketmq.common.message.MessageQueue;
-import org.apache.rocketmq.common.utils.ThreadUtils;
 import org.apache.rocketmq.remoting.protocol.NamespaceUtil;
 import org.apache.rocketmq.remoting.protocol.body.CMResult;
 import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult;
@@ -50,15 +45,12 @@ import 
org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel;
 import org.apache.rocketmq.logging.org.slf4j.Logger;
 import org.apache.rocketmq.logging.org.slf4j.LoggerFactory;
 
-public class ConsumeMessageOrderlyService implements ConsumeMessageService {
+public class ConsumeMessageOrderlyService extends 
AbstractConsumeMessageService {
     private static final Logger log = 
LoggerFactory.getLogger(ConsumeMessageOrderlyService.class);
     private final static long MAX_TIME_CONSUME_CONTINUOUSLY =
         
Long.parseLong(System.getProperty("rocketmq.client.maxTimeConsumeContinuously", 
"60000"));
     private final DefaultMQPushConsumerImpl defaultMQPushConsumerImpl;
-    private final DefaultMQPushConsumer defaultMQPushConsumer;
     private final MessageListenerOrderly messageListener;
-    private final BlockingQueue<Runnable> consumeRequestQueue;
-    private final ThreadPoolExecutor consumeExecutor;
     private final String consumerGroup;
     private final MessageQueueLock messageQueueLock = new MessageQueueLock();
     private final ScheduledExecutorService scheduledExecutorService;
@@ -66,22 +58,14 @@ public class ConsumeMessageOrderlyService implements 
ConsumeMessageService {
 
     public ConsumeMessageOrderlyService(DefaultMQPushConsumerImpl 
defaultMQPushConsumerImpl,
         MessageListenerOrderly messageListener) {
+        super(defaultMQPushConsumerImpl.getDefaultMQPushConsumer(), new 
ThreadFactoryImpl("ConsumeMessageThread_"
+            + 
getConsumerGroupTag(defaultMQPushConsumerImpl.getDefaultMQPushConsumer().getConsumerGroup())));
         this.defaultMQPushConsumerImpl = defaultMQPushConsumerImpl;
         this.messageListener = messageListener;
 
-        this.defaultMQPushConsumer = 
this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer();
         this.consumerGroup = this.defaultMQPushConsumer.getConsumerGroup();
-        this.consumeRequestQueue = new LinkedBlockingQueue<>();
-
-        String consumerGroupTag = (consumerGroup.length() > 100 ? 
consumerGroup.substring(0, 100) : consumerGroup) + "_";
-        this.consumeExecutor = new ThreadPoolExecutor(
-            this.defaultMQPushConsumer.getConsumeThreadMin(),
-            this.defaultMQPushConsumer.getConsumeThreadMax(),
-            1000 * 60,
-            TimeUnit.MILLISECONDS,
-            this.consumeRequestQueue,
-            new ThreadFactoryImpl("ConsumeMessageThread_" + consumerGroupTag));
 
+        String consumerGroupTag = getConsumerGroupTag(consumerGroup);
         this.scheduledExecutorService = 
Executors.newSingleThreadScheduledExecutor(new 
ThreadFactoryImpl("ConsumeMessageScheduledThread_" + consumerGroupTag));
     }
 
@@ -105,7 +89,7 @@ public class ConsumeMessageOrderlyService implements 
ConsumeMessageService {
     public void shutdown(long awaitTerminateMillis) {
         this.stopped = true;
         this.scheduledExecutorService.shutdown();
-        ThreadUtils.shutdownGracefully(this.consumeExecutor, 
awaitTerminateMillis, TimeUnit.MILLISECONDS);
+        shutdownConsumeExecutor(awaitTerminateMillis);
         if 
(MessageModel.CLUSTERING.equals(this.defaultMQPushConsumerImpl.messageModel())) 
{
             this.unlockAllMQ();
         }
@@ -115,28 +99,6 @@ public class ConsumeMessageOrderlyService implements 
ConsumeMessageService {
         this.defaultMQPushConsumerImpl.getRebalanceImpl().unlockAll(false);
     }
 
-    @Override
-    public void updateCorePoolSize(int corePoolSize) {
-        if (corePoolSize > 0
-            && corePoolSize <= Short.MAX_VALUE
-            && corePoolSize < 
this.defaultMQPushConsumer.getConsumeThreadMax()) {
-            this.consumeExecutor.setCorePoolSize(corePoolSize);
-        }
-    }
-
-    @Override
-    public void incCorePoolSize() {
-    }
-
-    @Override
-    public void decCorePoolSize() {
-    }
-
-    @Override
-    public int getCorePoolSize() {
-        return this.consumeExecutor.getCorePoolSize();
-    }
-
     @Override
     public ConsumeMessageDirectlyResult consumeMessageDirectly(MessageExt msg, 
String brokerName) {
         ConsumeMessageDirectlyResult result = new 
ConsumeMessageDirectlyResult();
diff --git 
a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopConcurrentlyService.java
 
b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopConcurrentlyService.java
index d519187110..9d70400903 100644
--- 
a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopConcurrentlyService.java
+++ 
b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopConcurrentlyService.java
@@ -20,16 +20,12 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
-import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.Executors;
-import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import org.apache.rocketmq.client.consumer.AckCallback;
 import org.apache.rocketmq.client.consumer.AckResult;
-import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
 import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
 import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
 import org.apache.rocketmq.client.consumer.listener.ConsumeReturnType;
@@ -43,40 +39,27 @@ import org.apache.rocketmq.common.message.MessageAccessor;
 import org.apache.rocketmq.common.message.MessageConst;
 import org.apache.rocketmq.common.message.MessageExt;
 import org.apache.rocketmq.common.message.MessageQueue;
-import org.apache.rocketmq.common.utils.ThreadUtils;
 import org.apache.rocketmq.remoting.protocol.body.CMResult;
 import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult;
 import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil;
 import org.apache.rocketmq.logging.org.slf4j.Logger;
 import org.apache.rocketmq.logging.org.slf4j.LoggerFactory;
 
-public class ConsumeMessagePopConcurrentlyService implements 
ConsumeMessageService {
+public class ConsumeMessagePopConcurrentlyService extends 
AbstractConsumeMessageService {
     private static final Logger log = 
LoggerFactory.getLogger(ConsumeMessagePopConcurrentlyService.class);
     private final DefaultMQPushConsumerImpl defaultMQPushConsumerImpl;
-    private final DefaultMQPushConsumer defaultMQPushConsumer;
     private final MessageListenerConcurrently messageListener;
-    private final BlockingQueue<Runnable> consumeRequestQueue;
-    private final ThreadPoolExecutor consumeExecutor;
     private final String consumerGroup;
 
     private final ScheduledExecutorService scheduledExecutorService;
 
     public ConsumeMessagePopConcurrentlyService(DefaultMQPushConsumerImpl 
defaultMQPushConsumerImpl,
         MessageListenerConcurrently messageListener) {
+        super(defaultMQPushConsumerImpl.getDefaultMQPushConsumer(), new 
ThreadFactoryImpl("ConsumeMessageThread_"));
         this.defaultMQPushConsumerImpl = defaultMQPushConsumerImpl;
         this.messageListener = messageListener;
 
-        this.defaultMQPushConsumer = 
this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer();
         this.consumerGroup = this.defaultMQPushConsumer.getConsumerGroup();
-        this.consumeRequestQueue = new LinkedBlockingQueue<>();
-
-        this.consumeExecutor = new ThreadPoolExecutor(
-            this.defaultMQPushConsumer.getConsumeThreadMin(),
-            this.defaultMQPushConsumer.getConsumeThreadMax(),
-            1000 * 60,
-            TimeUnit.MILLISECONDS,
-            this.consumeRequestQueue,
-            new ThreadFactoryImpl("ConsumeMessageThread_"));
 
         this.scheduledExecutorService = 
Executors.newSingleThreadScheduledExecutor(new 
ThreadFactoryImpl("ConsumeMessageScheduledThread_"));
     }
@@ -86,32 +69,9 @@ public class ConsumeMessagePopConcurrentlyService implements 
ConsumeMessageServi
 
     public void shutdown(long awaitTerminateMillis) {
         this.scheduledExecutorService.shutdown();
-        ThreadUtils.shutdownGracefully(this.consumeExecutor, 
awaitTerminateMillis, TimeUnit.MILLISECONDS);
-    }
-
-    @Override
-    public void updateCorePoolSize(int corePoolSize) {
-        if (corePoolSize > 0
-            && corePoolSize <= Short.MAX_VALUE
-            && corePoolSize < 
this.defaultMQPushConsumer.getConsumeThreadMax()) {
-            this.consumeExecutor.setCorePoolSize(corePoolSize);
-        }
-    }
-
-    @Override
-    public void incCorePoolSize() {
+        shutdownConsumeExecutor(awaitTerminateMillis);
     }
 
-    @Override
-    public void decCorePoolSize() {
-    }
-
-    @Override
-    public int getCorePoolSize() {
-        return this.consumeExecutor.getCorePoolSize();
-    }
-
-
     @Override
     public ConsumeMessageDirectlyResult consumeMessageDirectly(MessageExt msg, 
String brokerName) {
         ConsumeMessageDirectlyResult result = new 
ConsumeMessageDirectlyResult();
diff --git 
a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopOrderlyService.java
 
b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopOrderlyService.java
index 4eab1ccf66..8f6a5ee6df 100644
--- 
a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopOrderlyService.java
+++ 
b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopOrderlyService.java
@@ -19,14 +19,10 @@ package org.apache.rocketmq.client.impl.consumer;
 import io.netty.util.internal.ConcurrentSet;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.Executors;
-import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import org.apache.commons.lang3.StringUtils;
-import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
 import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext;
 import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
 import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
@@ -39,7 +35,6 @@ import org.apache.rocketmq.common.message.MessageAccessor;
 import org.apache.rocketmq.common.message.MessageConst;
 import org.apache.rocketmq.common.message.MessageExt;
 import org.apache.rocketmq.common.message.MessageQueue;
-import org.apache.rocketmq.common.utils.ThreadUtils;
 import org.apache.rocketmq.remoting.protocol.NamespaceUtil;
 import org.apache.rocketmq.remoting.protocol.body.CMResult;
 import org.apache.rocketmq.remoting.protocol.body.ConsumeMessageDirectlyResult;
@@ -47,14 +42,11 @@ import 
org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel;
 import org.apache.rocketmq.logging.org.slf4j.Logger;
 import org.apache.rocketmq.logging.org.slf4j.LoggerFactory;
 
-public class ConsumeMessagePopOrderlyService implements ConsumeMessageService {
+public class ConsumeMessagePopOrderlyService extends 
AbstractConsumeMessageService {
     private static final Logger log = 
LoggerFactory.getLogger(ConsumeMessagePopOrderlyService.class);
     private final DefaultMQPushConsumerImpl defaultMQPushConsumerImpl;
-    private final DefaultMQPushConsumer defaultMQPushConsumer;
     private final MessageListenerOrderly messageListener;
-    private final BlockingQueue<Runnable> consumeRequestQueue;
     private final ConcurrentSet<ConsumeRequest> consumeRequestSet = new 
ConcurrentSet<>();
-    private final ThreadPoolExecutor consumeExecutor;
     private final String consumerGroup;
     private final MessageQueueLock messageQueueLock = new MessageQueueLock();
     private final MessageQueueLock consumeRequestLock = new MessageQueueLock();
@@ -63,20 +55,11 @@ public class ConsumeMessagePopOrderlyService implements 
ConsumeMessageService {
 
     public ConsumeMessagePopOrderlyService(DefaultMQPushConsumerImpl 
defaultMQPushConsumerImpl,
         MessageListenerOrderly messageListener) {
+        super(defaultMQPushConsumerImpl.getDefaultMQPushConsumer(), new 
ThreadFactoryImpl("ConsumeMessageThread_"));
         this.defaultMQPushConsumerImpl = defaultMQPushConsumerImpl;
         this.messageListener = messageListener;
 
-        this.defaultMQPushConsumer = 
this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer();
         this.consumerGroup = this.defaultMQPushConsumer.getConsumerGroup();
-        this.consumeRequestQueue = new LinkedBlockingQueue<>();
-
-        this.consumeExecutor = new ThreadPoolExecutor(
-            this.defaultMQPushConsumer.getConsumeThreadMin(),
-            this.defaultMQPushConsumer.getConsumeThreadMax(),
-            1000 * 60,
-            TimeUnit.MILLISECONDS,
-            this.consumeRequestQueue,
-            new ThreadFactoryImpl("ConsumeMessageThread_"));
 
         this.scheduledExecutorService = 
Executors.newSingleThreadScheduledExecutor(new 
ThreadFactoryImpl("ConsumeMessageScheduledThread_"));
     }
@@ -97,7 +80,7 @@ public class ConsumeMessagePopOrderlyService implements 
ConsumeMessageService {
     public void shutdown(long awaitTerminateMillis) {
         this.stopped = true;
         this.scheduledExecutorService.shutdown();
-        ThreadUtils.shutdownGracefully(this.consumeExecutor, 
awaitTerminateMillis, TimeUnit.MILLISECONDS);
+        shutdownConsumeExecutor(awaitTerminateMillis);
         if 
(MessageModel.CLUSTERING.equals(this.defaultMQPushConsumerImpl.messageModel())) 
{
             this.unlockAllMessageQueues();
         }
@@ -107,28 +90,6 @@ public class ConsumeMessagePopOrderlyService implements 
ConsumeMessageService {
         this.defaultMQPushConsumerImpl.getRebalanceImpl().unlockAll(false);
     }
 
-    @Override
-    public void updateCorePoolSize(int corePoolSize) {
-        if (corePoolSize > 0
-            && corePoolSize <= Short.MAX_VALUE
-            && corePoolSize < 
this.defaultMQPushConsumer.getConsumeThreadMax()) {
-            this.consumeExecutor.setCorePoolSize(corePoolSize);
-        }
-    }
-
-    @Override
-    public void incCorePoolSize() {
-    }
-
-    @Override
-    public void decCorePoolSize() {
-    }
-
-    @Override
-    public int getCorePoolSize() {
-        return this.consumeExecutor.getCorePoolSize();
-    }
-
     @Override
     public ConsumeMessageDirectlyResult consumeMessageDirectly(MessageExt msg, 
String brokerName) {
         ConsumeMessageDirectlyResult result = new 
ConsumeMessageDirectlyResult();
diff --git 
a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageService.java
 
b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageService.java
index ee684730ae..1842b5eac1 100644
--- 
a/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageService.java
+++ 
b/client/src/main/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageService.java
@@ -32,6 +32,7 @@ public interface ConsumeMessageService {
 
     void decCorePoolSize();
 
+    /** Returns the owned pool core size, or -1 when execution is managed 
externally. */
     int getCorePoolSize();
 
     ConsumeMessageDirectlyResult consumeMessageDirectly(final MessageExt msg, 
final String brokerName);
diff --git 
a/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageExecutorInjectionTest.java
 
b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageExecutorInjectionTest.java
new file mode 100644
index 0000000000..8dfffdf8ba
--- /dev/null
+++ 
b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessageExecutorInjectionTest.java
@@ -0,0 +1,203 @@
+/*
+ * 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.client.impl.consumer;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
+import org.apache.rocketmq.client.consumer.store.OffsetStore;
+import org.apache.rocketmq.common.message.MessageExt;
+import org.apache.rocketmq.common.message.MessageQueue;
+import org.junit.Assume;
+import 
org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
+import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
+import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@RunWith(Parameterized.class)
+public class ConsumeMessageExecutorInjectionTest {
+    @Parameterized.Parameters(name = "{0}")
+    public static Collection<Object[]> services() {
+        return Arrays.asList(new Object[][] {
+            {ConsumeMessageConcurrentlyService.class, 
MessageListenerConcurrently.class},
+            {ConsumeMessageOrderlyService.class, MessageListenerOrderly.class},
+            {ConsumeMessagePopConcurrentlyService.class, 
MessageListenerConcurrently.class},
+            {ConsumeMessagePopOrderlyService.class, 
MessageListenerOrderly.class}
+        });
+    }
+
+    private final Class<? extends ConsumeMessageService> serviceClass;
+    private final Class<?> listenerClass;
+
+    public ConsumeMessageExecutorInjectionTest(Class<? extends 
ConsumeMessageService> serviceClass, Class<?> listenerClass) {
+        this.serviceClass = serviceClass;
+        this.listenerClass = listenerClass;
+    }
+
+    @Test
+    public void testSharingAndOwnership() throws Exception {
+        ThreadPoolExecutor shared = (ThreadPoolExecutor) 
Executors.newFixedThreadPool(1);
+        ConsumeMessageService first = createService(shared);
+        ConsumeMessageService second = createService(shared);
+        try {
+            ExecutorService firstExecutor = (ExecutorService) 
FieldUtils.readField(first, "consumeExecutor", true);
+            ExecutorService secondExecutor = (ExecutorService) 
FieldUtils.readField(second, "consumeExecutor", true);
+            assertSame(shared, firstExecutor);
+            assertSame(shared, secondExecutor);
+            Thread worker = firstExecutor.submit(Thread::currentThread).get(5, 
TimeUnit.SECONDS);
+            assertSame(worker, 
secondExecutor.submit(Thread::currentThread).get(5, TimeUnit.SECONDS));
+            first.updateCorePoolSize(10);
+            assertEquals(1, shared.getCorePoolSize());
+            assertEquals(-1, first.getCorePoolSize());
+            first.shutdown(5000);
+            assertFalse(shared.isShutdown());
+            assertSame(worker, 
secondExecutor.submit(Thread::currentThread).get(5, TimeUnit.SECONDS));
+        } finally {
+            first.shutdown(5000);
+            second.shutdown(5000);
+            shared.shutdownNow();
+        }
+    }
+
+    @Test
+    public void testCancellationDoesNotChangeOrdinaryConsumerOffsets() throws 
Exception {
+        Assume.assumeTrue(serviceClass == 
ConsumeMessageConcurrentlyService.class);
+        ThreadPoolExecutor shared = (ThreadPoolExecutor) 
Executors.newFixedThreadPool(1);
+        DefaultMQPushConsumer consumer = new 
DefaultMQPushConsumer("broadcast-discard-test");
+        consumer.setMessageModel(MessageModel.BROADCASTING);
+        consumer.setConsumeExecutor(shared);
+        DefaultMQPushConsumerImpl impl = mock(DefaultMQPushConsumerImpl.class);
+        when(impl.getDefaultMQPushConsumer()).thenReturn(consumer);
+        OffsetStore offsetStore = mock(OffsetStore.class);
+        when(impl.getOffsetStore()).thenReturn(offsetStore);
+        ConsumeMessageConcurrentlyService service = new 
ConsumeMessageConcurrentlyService(impl,
+            mock(MessageListenerConcurrently.class));
+        CountDownLatch entered = new CountDownLatch(1);
+        CountDownLatch release = new CountDownLatch(1);
+        try {
+            shared.submit(() -> {
+                entered.countDown();
+                try {
+                    release.await();
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+            });
+            assertTrue(entered.await(5, TimeUnit.SECONDS));
+            MessageQueue queue = new MessageQueue("system-topic", "broker", 0);
+            MessageExt message = new MessageExt();
+            message.setTopic(queue.getTopic());
+            message.setQueueOffset(0);
+            message.setBody(new byte[] {1});
+            ProcessQueue processQueue = new ProcessQueue();
+            processQueue.putMessage(Collections.singletonList(message));
+            service.submitConsumeRequest(Collections.singletonList(message), 
processQueue, queue, true);
+            assertTrue(((Future<?>) shared.getQueue().poll()).cancel(false));
+            assertEquals(1, processQueue.getMsgCount().get());
+            verifyNoInteractions(offsetStore);
+            service.shutdown(5000);
+            assertFalse(shared.isShutdown());
+        } finally {
+            release.countDown();
+            service.shutdown(5000);
+            shared.shutdownNow();
+        }
+    }
+
+    @Test
+    public void testVirtualThreadExecutorIsUsedDirectly() throws Exception {
+        Method factory;
+        try {
+            factory = 
Executors.class.getMethod("newVirtualThreadPerTaskExecutor");
+        } catch (NoSuchMethodException e) {
+            Assume.assumeNoException("Requires JDK 21 or later", e);
+            return;
+        }
+        ExecutorService shared = (ExecutorService) factory.invoke(null);
+        ConsumeMessageService service = createService(shared);
+        try {
+            ExecutorService actual = (ExecutorService) 
FieldUtils.readField(service, "consumeExecutor", true);
+            assertSame(shared, actual);
+            Method isVirtual = Thread.class.getMethod("isVirtual");
+            assertTrue(actual.submit(() -> (Boolean) 
isVirtual.invoke(Thread.currentThread())).get(5, TimeUnit.SECONDS));
+            service.shutdown(5000);
+            assertFalse(shared.isShutdown());
+            assertTrue(shared.submit(() -> (Boolean) 
isVirtual.invoke(Thread.currentThread())).get(5, TimeUnit.SECONDS));
+        } finally {
+            service.shutdown(5000);
+            shared.shutdownNow();
+        }
+    }
+
+    @Test
+    public void testExternalTasksAreLeftToTheOwnerOnShutdown() throws 
Exception {
+        ExecutorService shared = Executors.newSingleThreadExecutor();
+        ConsumeMessageService service = createService(shared);
+        CountDownLatch entered = new CountDownLatch(1);
+        CountDownLatch release = new CountDownLatch(1);
+        try {
+            Future<?> running = shared.submit(() -> {
+                entered.countDown();
+                try {
+                    release.await();
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+            });
+            assertTrue(entered.await(5, TimeUnit.SECONDS));
+            service.shutdown(0);
+            assertFalse(shared.isShutdown());
+            assertFalse(running.isDone());
+            release.countDown();
+            running.get(5, TimeUnit.SECONDS);
+        } finally {
+            release.countDown();
+            service.shutdown(0);
+            shared.shutdownNow();
+        }
+    }
+
+    private ConsumeMessageService createService(ExecutorService executor) 
throws Exception {
+        DefaultMQPushConsumer consumer = new 
DefaultMQPushConsumer("shared-executor-test");
+        consumer.setMessageModel(MessageModel.BROADCASTING);
+        consumer.setConsumeExecutor(executor);
+        DefaultMQPushConsumerImpl impl = mock(DefaultMQPushConsumerImpl.class);
+        when(impl.getDefaultMQPushConsumer()).thenReturn(consumer);
+        when(impl.messageModel()).thenReturn(MessageModel.BROADCASTING);
+        return serviceClass.getConstructor(DefaultMQPushConsumerImpl.class, 
listenerClass)
+            .newInstance(impl, mock(listenerClass));
+    }
+}
diff --git 
a/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopConcurrentlyServiceTest.java
 
b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopConcurrentlyServiceTest.java
index 5097f14ca3..1d13c6496d 100644
--- 
a/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopConcurrentlyServiceTest.java
+++ 
b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopConcurrentlyServiceTest.java
@@ -123,7 +123,7 @@ public class ConsumeMessagePopConcurrentlyServiceTest {
     public void testShutdown() throws IllegalAccessException {
         popService.shutdown(3000L);
         Field scheduledExecutorServiceField = 
FieldUtils.getDeclaredField(popService.getClass(), "scheduledExecutorService", 
true);
-        Field consumeExecutorField = 
FieldUtils.getDeclaredField(popService.getClass(), "consumeExecutor", true);
+        Field consumeExecutorField = 
FieldUtils.getField(popService.getClass(), "consumeExecutor", true);
         ScheduledExecutorService scheduledExecutorService = 
(ScheduledExecutorService) scheduledExecutorServiceField.get(popService);
         ThreadPoolExecutor consumeExecutor = (ThreadPoolExecutor) 
consumeExecutorField.get(popService);
         assertTrue(scheduledExecutorService.isShutdown());
@@ -148,7 +148,7 @@ public class ConsumeMessagePopConcurrentlyServiceTest {
         PopProcessQueue processQueue = mock(PopProcessQueue.class);
         MessageQueue messageQueue = mock(MessageQueue.class);
         ThreadPoolExecutor consumeExecutor = mock(ThreadPoolExecutor.class);
-        FieldUtils.writeDeclaredField(popService, "consumeExecutor", 
consumeExecutor, true);
+        FieldUtils.writeField(popService, "consumeExecutor", consumeExecutor, 
true);
         popService.submitPopConsumeRequest(msgs, processQueue, messageQueue);
         verify(consumeExecutor, times(1)).submit(any(Runnable.class));
     }
@@ -159,7 +159,7 @@ public class ConsumeMessagePopConcurrentlyServiceTest {
         PopProcessQueue processQueue = mock(PopProcessQueue.class);
         MessageQueue messageQueue = mock(MessageQueue.class);
         ThreadPoolExecutor consumeExecutor = mock(ThreadPoolExecutor.class);
-        FieldUtils.writeDeclaredField(popService, "consumeExecutor", 
consumeExecutor, true);
+        FieldUtils.writeField(popService, "consumeExecutor", consumeExecutor, 
true);
         
when(defaultMQPushConsumer.getConsumeMessageBatchMaxSize()).thenReturn(1);
         popService.submitPopConsumeRequest(msgs, processQueue, messageQueue);
         verify(consumeExecutor, times(2)).submit(any(Runnable.class));
diff --git 
a/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopOrderlyServiceTest.java
 
b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopOrderlyServiceTest.java
index 257783ecb4..5ada0639f2 100644
--- 
a/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopOrderlyServiceTest.java
+++ 
b/client/src/test/java/org/apache/rocketmq/client/impl/consumer/ConsumeMessagePopOrderlyServiceTest.java
@@ -100,7 +100,7 @@ public class ConsumeMessagePopOrderlyServiceTest {
     public void testShutdown() throws IllegalAccessException {
         popService.shutdown(3000L);
         Field scheduledExecutorServiceField = 
FieldUtils.getDeclaredField(popService.getClass(), "scheduledExecutorService", 
true);
-        Field consumeExecutorField = 
FieldUtils.getDeclaredField(popService.getClass(), "consumeExecutor", true);
+        Field consumeExecutorField = 
FieldUtils.getField(popService.getClass(), "consumeExecutor", true);
         ScheduledExecutorService scheduledExecutorService = 
(ScheduledExecutorService) scheduledExecutorServiceField.get(popService);
         ThreadPoolExecutor consumeExecutor = (ThreadPoolExecutor) 
consumeExecutorField.get(popService);
         assertTrue(scheduledExecutorService.isShutdown());
@@ -183,7 +183,7 @@ public class ConsumeMessagePopOrderlyServiceTest {
         PopProcessQueue processQueue = mock(PopProcessQueue.class);
         MessageQueue messageQueue = mock(MessageQueue.class);
         ThreadPoolExecutor consumeExecutor = mock(ThreadPoolExecutor.class);
-        FieldUtils.writeDeclaredField(popService, "consumeExecutor", 
consumeExecutor, true);
+        FieldUtils.writeField(popService, "consumeExecutor", consumeExecutor, 
true);
         popService.submitPopConsumeRequest(msgs, processQueue, messageQueue);
         verify(consumeExecutor, times(1)).submit(any(Runnable.class));
     }
diff --git 
a/proxy/src/main/java/org/apache/rocketmq/proxy/config/ProxyConfig.java 
b/proxy/src/main/java/org/apache/rocketmq/proxy/config/ProxyConfig.java
index a7896c11e0..8f6fb7ec1b 100644
--- a/proxy/src/main/java/org/apache/rocketmq/proxy/config/ProxyConfig.java
+++ b/proxy/src/main/java/org/apache/rocketmq/proxy/config/ProxyConfig.java
@@ -63,6 +63,7 @@ public class ProxyConfig implements ConfigFile {
     private String heartbeatSyncerTopicClusterName = "";
     private int heartbeatSyncerThreadPoolNums = 4;
     private int heartbeatSyncerThreadPoolQueueCapacity = 100;
+    private int systemMessageConsumerThreadPoolCoreSize = PROCESSOR_NUMBER * 2;
 
     private String heartbeatSyncerTopicName = "DefaultHeartBeatSyncerTopic";
 
@@ -395,6 +396,14 @@ public class ProxyConfig implements ConfigFile {
         this.heartbeatSyncerTopicClusterName = heartbeatSyncerTopicClusterName;
     }
 
+    public int getSystemMessageConsumerThreadPoolCoreSize() {
+        return systemMessageConsumerThreadPoolCoreSize;
+    }
+
+    public void setSystemMessageConsumerThreadPoolCoreSize(int 
systemMessageConsumerThreadPoolCoreSize) {
+        this.systemMessageConsumerThreadPoolCoreSize = 
systemMessageConsumerThreadPoolCoreSize;
+    }
+
     public int getHeartbeatSyncerThreadPoolNums() {
         return heartbeatSyncerThreadPoolNums;
     }
diff --git 
a/proxy/src/main/java/org/apache/rocketmq/proxy/service/ClusterServiceManager.java
 
b/proxy/src/main/java/org/apache/rocketmq/proxy/service/ClusterServiceManager.java
index 8b1c20c0bd..89539244f2 100644
--- 
a/proxy/src/main/java/org/apache/rocketmq/proxy/service/ClusterServiceManager.java
+++ 
b/proxy/src/main/java/org/apache/rocketmq/proxy/service/ClusterServiceManager.java
@@ -17,6 +17,7 @@
 package org.apache.rocketmq.proxy.service;
 
 import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import org.apache.rocketmq.broker.client.ClientChannelInfo;
 import org.apache.rocketmq.broker.client.ConsumerGroupEvent;
@@ -50,6 +51,7 @@ import 
org.apache.rocketmq.proxy.service.relay.ClusterProxyRelayService;
 import org.apache.rocketmq.proxy.service.relay.ProxyRelayService;
 import org.apache.rocketmq.proxy.service.route.ClusterTopicRouteService;
 import org.apache.rocketmq.proxy.service.route.TopicRouteService;
+import 
org.apache.rocketmq.proxy.service.sysmessage.SystemMessageConsumeExecutor;
 import org.apache.rocketmq.proxy.service.transaction.ClusterTransactionService;
 import org.apache.rocketmq.proxy.service.transaction.TransactionService;
 import org.apache.rocketmq.remoting.RPCHook;
@@ -69,6 +71,7 @@ public class ClusterServiceManager extends 
AbstractStartAndShutdown implements S
     protected LiteSubscriptionService liteSubscriptionService;
 
     protected ScheduledExecutorService scheduledExecutorService;
+    protected ThreadPoolExecutor systemMessageConsumeExecutor;
     protected MQClientAPIFactory messagingClientAPIFactory;
     protected MQClientAPIFactory operationClientAPIFactory;
     protected MQClientAPIFactory transactionClientAPIFactory;
@@ -109,8 +112,9 @@ public class ClusterServiceManager extends 
AbstractStartAndShutdown implements S
         this.metadataService = new ClusterMetadataService(topicRouteService, 
operationClientAPIFactory);
         this.adminService = new 
DefaultAdminService(this.operationClientAPIFactory);
 
+        this.systemMessageConsumeExecutor = 
SystemMessageConsumeExecutor.create(proxyConfig);
         this.producerManager = new ProducerManager();
-        this.consumerManager = new 
ClusterConsumerManager(this.topicRouteService, this.adminService, 
this.operationClientAPIFactory, new ConsumerIdsChangeListenerImpl(), 
proxyConfig.getChannelExpiredTimeout(), rpcHook);
+        this.consumerManager = new 
ClusterConsumerManager(this.topicRouteService, this.adminService, 
this.operationClientAPIFactory, new ConsumerIdsChangeListenerImpl(), 
proxyConfig.getChannelExpiredTimeout(), rpcHook, 
this.systemMessageConsumeExecutor);
 
         this.transactionClientAPIFactory = new MQClientAPIFactory(
             nameserverAccessConfig,
@@ -159,6 +163,7 @@ public class ClusterServiceManager extends 
AbstractStartAndShutdown implements S
         this.appendStartAndShutdown(this.topicRouteService);
         this.appendStartAndShutdown(this.clusterTransactionService);
         this.appendStartAndShutdown(this.metadataService);
+        this.appendShutdown(() -> 
ThreadUtils.shutdownGracefully(this.systemMessageConsumeExecutor, 5, 
TimeUnit.SECONDS));
         this.appendStartAndShutdown(this.consumerManager);
     }
 
diff --git 
a/proxy/src/main/java/org/apache/rocketmq/proxy/service/client/ClusterConsumerManager.java
 
b/proxy/src/main/java/org/apache/rocketmq/proxy/service/client/ClusterConsumerManager.java
index 65a4569f83..d71aabdbec 100644
--- 
a/proxy/src/main/java/org/apache/rocketmq/proxy/service/client/ClusterConsumerManager.java
+++ 
b/proxy/src/main/java/org/apache/rocketmq/proxy/service/client/ClusterConsumerManager.java
@@ -18,6 +18,7 @@
 package org.apache.rocketmq.proxy.service.client;
 
 import java.util.Set;
+import java.util.concurrent.ExecutorService;
 import org.apache.rocketmq.broker.client.ClientChannelInfo;
 import org.apache.rocketmq.broker.client.ConsumerIdsChangeListener;
 import org.apache.rocketmq.broker.client.ConsumerManager;
@@ -38,8 +39,15 @@ public class ClusterConsumerManager extends ConsumerManager 
implements StartAndS
 
     public ClusterConsumerManager(TopicRouteService topicRouteService, 
AdminService adminService,
                                   MQClientAPIFactory mqClientAPIFactory, 
ConsumerIdsChangeListener consumerIdsChangeListener, long 
channelExpiredTimeout, RPCHook rpcHook) {
+        this(topicRouteService, adminService, mqClientAPIFactory, 
consumerIdsChangeListener,
+            channelExpiredTimeout, rpcHook, null);
+    }
+
+    public ClusterConsumerManager(TopicRouteService topicRouteService, 
AdminService adminService,
+        MQClientAPIFactory mqClientAPIFactory, ConsumerIdsChangeListener 
consumerIdsChangeListener,
+        long channelExpiredTimeout, RPCHook rpcHook, ExecutorService 
consumeExecutor) {
         super(consumerIdsChangeListener, channelExpiredTimeout);
-        this.heartbeatSyncer = new HeartbeatSyncer(topicRouteService, 
adminService, this, mqClientAPIFactory, rpcHook);
+        this.heartbeatSyncer = new HeartbeatSyncer(topicRouteService, 
adminService, this, mqClientAPIFactory, rpcHook, consumeExecutor);
     }
 
     @Override
diff --git 
a/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/AbstractSystemMessageSyncer.java
 
b/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/AbstractSystemMessageSyncer.java
index 05eb672618..e91fe57798 100644
--- 
a/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/AbstractSystemMessageSyncer.java
+++ 
b/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/AbstractSystemMessageSyncer.java
@@ -46,6 +46,7 @@ import 
org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel;
 
 import java.nio.charset.StandardCharsets;
 import java.time.Duration;
+import java.util.concurrent.ExecutorService;
 
 public abstract class AbstractSystemMessageSyncer implements StartAndShutdown, 
MessageListenerConcurrently {
     protected static final Logger log = 
LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME);
@@ -53,9 +54,16 @@ public abstract class AbstractSystemMessageSyncer implements 
StartAndShutdown, M
     protected final AdminService adminService;
     protected final MQClientAPIFactory mqClientAPIFactory;
     protected final RPCHook rpcHook;
+    protected final ExecutorService consumeExecutor;
     protected DefaultMQPushConsumer defaultMQPushConsumer;
 
     public AbstractSystemMessageSyncer(TopicRouteService topicRouteService, 
AdminService adminService, MQClientAPIFactory mqClientAPIFactory, RPCHook 
rpcHook) {
+        this(topicRouteService, adminService, mqClientAPIFactory, rpcHook, 
null);
+    }
+
+    public AbstractSystemMessageSyncer(TopicRouteService topicRouteService, 
AdminService adminService,
+        MQClientAPIFactory mqClientAPIFactory, RPCHook rpcHook, 
ExecutorService consumeExecutor) {
+        this.consumeExecutor = consumeExecutor;
         this.topicRouteService = topicRouteService;
         this.adminService = adminService;
         this.mqClientAPIFactory = mqClientAPIFactory;
@@ -145,6 +153,7 @@ public abstract class AbstractSystemMessageSyncer 
implements StartAndShutdown, M
         RPCHook rpcHook = this.getRpcHook();
         this.defaultMQPushConsumer = new 
DefaultMQPushConsumer(this.getSystemMessageConsumerId(), rpcHook);
 
+        this.defaultMQPushConsumer.setConsumeExecutor(this.consumeExecutor);
         
this.defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
         this.defaultMQPushConsumer.setMessageModel(MessageModel.BROADCASTING);
         try {
diff --git 
a/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/HeartbeatSyncer.java
 
b/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/HeartbeatSyncer.java
index e063d79707..5d9fbb3069 100644
--- 
a/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/HeartbeatSyncer.java
+++ 
b/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/HeartbeatSyncer.java
@@ -45,6 +45,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 
@@ -57,7 +58,13 @@ public class HeartbeatSyncer extends 
AbstractSystemMessageSyncer {
 
     public HeartbeatSyncer(TopicRouteService topicRouteService, AdminService 
adminService,
                            ConsumerManager consumerManager, MQClientAPIFactory 
mqClientAPIFactory, RPCHook rpcHook) {
-        super(topicRouteService, adminService, mqClientAPIFactory, rpcHook);
+        this(topicRouteService, adminService, consumerManager, 
mqClientAPIFactory, rpcHook, null);
+    }
+
+    public HeartbeatSyncer(TopicRouteService topicRouteService, AdminService 
adminService,
+        ConsumerManager consumerManager, MQClientAPIFactory 
mqClientAPIFactory, RPCHook rpcHook,
+        ExecutorService consumeExecutor) {
+        super(topicRouteService, adminService, mqClientAPIFactory, rpcHook, 
consumeExecutor);
         this.consumerManager = consumerManager;
         this.localProxyId = buildLocalProxyId();
         this.init();
diff --git 
a/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/SystemMessageConsumeExecutor.java
 
b/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/SystemMessageConsumeExecutor.java
new file mode 100644
index 0000000000..88805cf318
--- /dev/null
+++ 
b/proxy/src/main/java/org/apache/rocketmq/proxy/service/sysmessage/SystemMessageConsumeExecutor.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.proxy.service.sysmessage;
+
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import org.apache.rocketmq.common.thread.ThreadPoolMonitor;
+import org.apache.rocketmq.proxy.config.ProxyConfig;
+
+/** Creates the executor shared by a Proxy's internal system-message 
consumers. */
+public class SystemMessageConsumeExecutor {
+    private SystemMessageConsumeExecutor() {
+    }
+
+    public static ThreadPoolExecutor create(ProxyConfig config) {
+        int coreSize = config.getSystemMessageConsumerThreadPoolCoreSize();
+        return ThreadPoolMonitor.createAndMonitor(
+            coreSize, coreSize,
+            0, TimeUnit.MILLISECONDS, "SystemMessageConsumer",
+            // LinkedBlockingQueue's default capacity preserves unbounded 
consumption queueing.
+            Integer.MAX_VALUE,
+            new ThreadPoolExecutor.AbortPolicy());
+    }
+}
diff --git 
a/proxy/src/test/java/org/apache/rocketmq/proxy/service/sysmessage/SystemMessageConsumeExecutorTest.java
 
b/proxy/src/test/java/org/apache/rocketmq/proxy/service/sysmessage/SystemMessageConsumeExecutorTest.java
new file mode 100644
index 0000000000..3c61c9ed5f
--- /dev/null
+++ 
b/proxy/src/test/java/org/apache/rocketmq/proxy/service/sysmessage/SystemMessageConsumeExecutorTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.proxy.service.sysmessage;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import org.apache.rocketmq.proxy.config.ProxyConfig;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class SystemMessageConsumeExecutorTest {
+    @Test
+    public void testDefaults() {
+        ProxyConfig config = new ProxyConfig();
+        int processors = Runtime.getRuntime().availableProcessors();
+        ThreadPoolExecutor executor = 
SystemMessageConsumeExecutor.create(config);
+        try {
+            assertEquals(processors * 2, executor.getCorePoolSize());
+            assertEquals(processors * 2, executor.getMaximumPoolSize());
+            assertEquals(Integer.MAX_VALUE, 
executor.getQueue().remainingCapacity());
+            assertFalse(executor.allowsCoreThreadTimeOut());
+            assertTrue(executor.getRejectedExecutionHandler() instanceof 
ThreadPoolExecutor.AbortPolicy);
+        } finally {
+            executor.shutdownNow();
+        }
+    }
+
+    @Test
+    public void testConfiguredPoolPreservesQueuedTasks() throws Exception {
+        ProxyConfig config = new ProxyConfig();
+        config.setSystemMessageConsumerThreadPoolCoreSize(1);
+        ThreadPoolExecutor executor = 
SystemMessageConsumeExecutor.create(config);
+        CountDownLatch entered = new CountDownLatch(1);
+        CountDownLatch release = new CountDownLatch(1);
+        try {
+            Future<?> running = executor.submit(() -> {
+                entered.countDown();
+                try {
+                    release.await();
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+            });
+            assertTrue(entered.await(5, TimeUnit.SECONDS));
+            List<Future<Integer>> queued = new ArrayList<>();
+            for (int i = 0; i < 10001; i++) {
+                final int result = i;
+                queued.add(executor.submit(() -> result));
+            }
+            assertEquals(10001, executor.getQueue().size());
+            assertEquals(1, executor.getPoolSize());
+            assertFalse(running.isCancelled());
+            assertFalse(queued.get(0).isDone());
+            release.countDown();
+            executor.shutdown();
+            assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
+            running.get(5, TimeUnit.SECONDS);
+            for (int i = 0; i < queued.size(); i++) {
+                assertEquals(i, queued.get(i).get(5, 
TimeUnit.SECONDS).intValue());
+            }
+            try {
+                executor.submit(() -> { });
+                fail("Stopped executor must reject submission");
+            } catch (RejectedExecutionException expected) {
+                assertTrue(executor.isTerminated());
+            }
+        } finally {
+            release.countDown();
+            executor.shutdownNow();
+        }
+    }
+}
diff --git 
a/proxy/src/test/java/org/apache/rocketmq/proxy/service/sysmessage/SystemMessageConsumerSharingTest.java
 
b/proxy/src/test/java/org/apache/rocketmq/proxy/service/sysmessage/SystemMessageConsumerSharingTest.java
new file mode 100644
index 0000000000..4cbb4685e8
--- /dev/null
+++ 
b/proxy/src/test/java/org/apache/rocketmq/proxy/service/sysmessage/SystemMessageConsumerSharingTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.proxy.service.sysmessage;
+
+import java.util.List;
+import java.util.concurrent.ThreadPoolExecutor;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.rocketmq.broker.client.ConsumerIdsChangeListener;
+import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
+import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
+import org.apache.rocketmq.client.impl.mqclient.MQClientAPIFactory;
+import org.apache.rocketmq.common.message.MessageExt;
+import org.apache.rocketmq.proxy.config.ConfigurationManager;
+import org.apache.rocketmq.proxy.config.InitConfigTest;
+import org.apache.rocketmq.proxy.service.admin.AdminService;
+import org.apache.rocketmq.proxy.service.client.ClusterConsumerManager;
+import org.apache.rocketmq.proxy.service.route.TopicRouteService;
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.mock;
+
+public class SystemMessageConsumerSharingTest extends InitConfigTest {
+    @Test
+    public void testManagerAndAdditionalSyncerReceiveSameExecutor() throws 
Exception {
+        ThreadPoolExecutor executor = 
SystemMessageConsumeExecutor.create(ConfigurationManager.getProxyConfig());
+        TopicRouteService routeService = mock(TopicRouteService.class);
+        AdminService adminService = mock(AdminService.class);
+        MQClientAPIFactory clientFactory = mock(MQClientAPIFactory.class);
+        ClusterConsumerManager manager = new 
ClusterConsumerManager(routeService, adminService, clientFactory,
+            mock(ConsumerIdsChangeListener.class), 120000, null, executor);
+        HeartbeatSyncer heartbeat = (HeartbeatSyncer) 
FieldUtils.readDeclaredField(manager, "heartbeatSyncer", true);
+        AbstractSystemMessageSyncer additional = new 
AbstractSystemMessageSyncer(routeService, adminService,
+            clientFactory, null, executor) {
+            @Override
+            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> 
messages, ConsumeConcurrentlyContext context) {
+                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
+            }
+        };
+        try {
+            assertSame(executor, heartbeat.consumeExecutor);
+            assertSame(executor, additional.consumeExecutor);
+            assertFalse(executor.isShutdown());
+        } finally {
+            heartbeat.threadPoolExecutor.shutdownNow();
+            executor.shutdownNow();
+        }
+    }
+}

Reply via email to