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

lizhimins 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 eedd7607de [ISSUE #11083] Release owned gRPC event loops and stabilize 
POP priority tests (#11084)
eedd7607de is described below

commit eedd7607de97368c0dcb45a4c28867ce3226df24
Author: qianye <[email protected]>
AuthorDate: Thu Sep 10 13:45:34 2026 +0800

    [ISSUE #11083] Release owned gRPC event loops and stabilize POP priority 
tests (#11084)
---
 .github/workflows/bazel.yml                        |  10 +-
 proxy/BUILD.bazel                                  |   1 +
 .../org/apache/rocketmq/proxy/grpc/GrpcServer.java |  41 +++-
 .../rocketmq/proxy/grpc/GrpcServerBuilder.java     |  95 +++++++--
 .../apache/rocketmq/proxy/grpc/GrpcServerTest.java | 231 +++++++++++++++++++++
 .../test/client/consumer/pop/PopPriorityIT.java    |  99 ++++++---
 6 files changed, 428 insertions(+), 49 deletions(-)

diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml
index 666512017e..a19bcfc7d5 100644
--- a/.github/workflows/bazel.yml
+++ b/.github/workflows/bazel.yml
@@ -32,4 +32,12 @@ jobs:
       - name: Build
         run: bazel build --config=remote //...
       - name: Run Tests
-        run: bazel test --config=remote //...
\ No newline at end of file
+        run: bazel test --config=remote //...
+      - name: Upload Bazel test results
+        if: failure()
+        uses: actions/upload-artifact@v4
+        with:
+          name: bazel-test-results
+          path: bazel-testlogs/**/test.xml
+          if-no-files-found: ignore
+          retention-days: 7
diff --git a/proxy/BUILD.bazel b/proxy/BUILD.bazel
index 0711d48a66..8ddaa52382 100644
--- a/proxy/BUILD.bazel
+++ b/proxy/BUILD.bazel
@@ -95,6 +95,7 @@ java_library(
         "@maven//:io_grpc_grpc_api",
         "@maven//:io_grpc_grpc_context",
         "@maven//:io_grpc_grpc_netty_shaded",
+        "@maven//:io_grpc_grpc_protobuf",
         "@maven//:io_grpc_grpc_stub",
         "@maven//:io_netty_netty_all",
         "@maven//:io_github_aliyunmq_rocketmq_grpc_netty_codec_haproxy",
diff --git a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/GrpcServer.java 
b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/GrpcServer.java
index af3d6b4c6c..74b71b14ee 100644
--- a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/GrpcServer.java
+++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/GrpcServer.java
@@ -19,6 +19,7 @@ package org.apache.rocketmq.proxy.grpc;
 
 import com.google.common.annotations.VisibleForTesting;
 import io.grpc.Server;
+import io.grpc.netty.shaded.io.netty.channel.EventLoopGroup;
 import org.apache.rocketmq.common.constant.LoggerName;
 import org.apache.rocketmq.common.utils.StartAndShutdown;
 import org.apache.rocketmq.logging.org.slf4j.Logger;
@@ -33,6 +34,8 @@ public class GrpcServer implements StartAndShutdown {
     private static final Logger log = 
LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME);
 
     private final Server server;
+    private final EventLoopGroup ownedBossGroup;
+    private final EventLoopGroup ownedWorkerGroup;
 
     private final long timeout;
 
@@ -43,7 +46,14 @@ public class GrpcServer implements StartAndShutdown {
 
     protected GrpcServer(Server server, long timeout, TimeUnit unit,
         TlsCertificateManager tlsCertificateManager) throws Exception {
+        this(server, timeout, unit, tlsCertificateManager, null, null);
+    }
+
+    GrpcServer(Server server, long timeout, TimeUnit unit, 
TlsCertificateManager tlsCertificateManager,
+        EventLoopGroup ownedBossGroup, EventLoopGroup ownedWorkerGroup) throws 
Exception {
         this.server = server;
+        this.ownedBossGroup = ownedBossGroup;
+        this.ownedWorkerGroup = ownedWorkerGroup;
         this.timeout = timeout;
         this.unit = unit;
         this.tlsCertificateManager = tlsCertificateManager;
@@ -51,11 +61,15 @@ public class GrpcServer implements StartAndShutdown {
     }
 
     public void start() throws Exception {
-        // Register the TLS context reload handler
-        tlsCertificateManager.registerReloadListener(this.tlsReloadHandler);
-
-        this.server.start();
-        log.info("grpc server start successfully.");
+        try {
+            // Register the TLS context reload handler
+            
tlsCertificateManager.registerReloadListener(this.tlsReloadHandler);
+            this.server.start();
+            log.info("grpc server start successfully.");
+        } catch (Exception | Error e) {
+            shutdown();
+            throw e;
+        }
     }
 
     public void shutdown() {
@@ -63,12 +77,25 @@ public class GrpcServer implements StartAndShutdown {
             // Unregister the TLS context reload handler
             
tlsCertificateManager.unregisterReloadListener(this.tlsReloadHandler);
 
-            this.server.shutdown().awaitTermination(timeout, unit);
+            if (!this.server.shutdown().awaitTermination(timeout, unit)) {
+                this.server.shutdownNow().awaitTermination(timeout, unit);
+            }
 
             log.info("grpc server shutdown successfully.");
+        } catch (InterruptedException e) {
+            this.server.shutdownNow();
+            Thread.currentThread().interrupt();
+            log.error("Interrupted while shutting down grpc server", e);
         } catch (Exception e) {
-            e.printStackTrace();
+            this.server.shutdownNow();
             log.error("Failed to shutdown grpc server", e);
+        } finally {
+            if (ownedBossGroup != null) {
+                ownedBossGroup.shutdownGracefully();
+            }
+            if (ownedWorkerGroup != null) {
+                ownedWorkerGroup.shutdownGracefully();
+            }
         }
     }
 
diff --git 
a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/GrpcServerBuilder.java 
b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/GrpcServerBuilder.java
index f59c4982c4..35d035b993 100644
--- a/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/GrpcServerBuilder.java
+++ b/proxy/src/main/java/org/apache/rocketmq/proxy/grpc/GrpcServerBuilder.java
@@ -20,10 +20,12 @@ import io.grpc.BindableService;
 import io.grpc.ServerInterceptor;
 import io.grpc.ServerServiceDefinition;
 import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
+import io.grpc.netty.shaded.io.netty.channel.EventLoopGroup;
 import io.grpc.netty.shaded.io.netty.channel.epoll.EpollEventLoopGroup;
 import io.grpc.netty.shaded.io.netty.channel.epoll.EpollServerSocketChannel;
 import io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoopGroup;
 import io.grpc.netty.shaded.io.netty.channel.socket.nio.NioServerSocketChannel;
+import java.util.Objects;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import org.apache.rocketmq.common.constant.LoggerName;
@@ -39,6 +41,11 @@ import 
org.apache.rocketmq.proxy.service.cert.TlsCertificateManager;
 public class GrpcServerBuilder {
     private static final Logger log = 
LoggerFactory.getLogger(LoggerName.PROXY_LOGGER_NAME);
     protected NettyServerBuilder serverBuilder;
+    private final int bossLoopNum;
+    private final int workerLoopNum;
+    private final boolean enableEpoll;
+    private final EventLoopGroup bossGroup;
+    private final EventLoopGroup workerGroup;
 
     protected long time = 30;
 
@@ -51,8 +58,50 @@ public class GrpcServerBuilder {
         return new GrpcServerBuilder(executor, port, tlsCertificateManager);
     }
 
+    /**
+     * Creates a server with internally owned event loops. Zero selects 
Netty's default thread count.
+     */
+    public static GrpcServerBuilder newBuilder(ThreadPoolExecutor executor, 
int port,
+        TlsCertificateManager tlsCertificateManager, int bossLoopNum, int 
workerLoopNum) {
+        return new GrpcServerBuilder(executor, port, tlsCertificateManager, 
bossLoopNum, workerLoopNum);
+    }
+
+    /**
+     * Creates a server using caller-owned event loops. Both groups must match 
enableGrpcEpoll.
+     * The caller must close them after all servers using them have terminated.
+     */
+    public static GrpcServerBuilder newBuilder(ThreadPoolExecutor executor, 
int port,
+        TlsCertificateManager tlsCertificateManager, EventLoopGroup bossGroup, 
EventLoopGroup workerGroup) {
+        return new GrpcServerBuilder(executor, port, tlsCertificateManager, 
bossGroup, workerGroup);
+    }
+
     protected GrpcServerBuilder(ThreadPoolExecutor executor, int port, 
TlsCertificateManager tlsCertificateManager) {
+        this(executor, port, tlsCertificateManager, 
ConfigurationManager.getProxyConfig().getGrpcBossLoopNum(),
+            ConfigurationManager.getProxyConfig().getGrpcWorkerLoopNum());
+    }
+
+    protected GrpcServerBuilder(ThreadPoolExecutor executor, int port, 
TlsCertificateManager tlsCertificateManager,
+        int bossLoopNum, int workerLoopNum) {
+        this(executor, port, tlsCertificateManager, bossLoopNum, 
workerLoopNum, null, null);
+    }
+
+    protected GrpcServerBuilder(ThreadPoolExecutor executor, int port, 
TlsCertificateManager tlsCertificateManager,
+        EventLoopGroup bossGroup, EventLoopGroup workerGroup) {
+        this(executor, port, tlsCertificateManager, 0, 0,
+            Objects.requireNonNull(bossGroup, "bossGroup"), 
Objects.requireNonNull(workerGroup, "workerGroup"));
+    }
+
+    private GrpcServerBuilder(ThreadPoolExecutor executor, int port, 
TlsCertificateManager tlsCertificateManager,
+        int bossLoopNum, int workerLoopNum, EventLoopGroup bossGroup, 
EventLoopGroup workerGroup) {
+        if (bossLoopNum < 0 || workerLoopNum < 0) {
+            throw new IllegalArgumentException("Event loop thread counts must 
not be negative");
+        }
         ProxyConfig config = ConfigurationManager.getProxyConfig();
+        this.bossLoopNum = bossLoopNum;
+        this.workerLoopNum = workerLoopNum;
+        this.enableEpoll = config.isEnableGrpcEpoll();
+        this.bossGroup = bossGroup;
+        this.workerGroup = workerGroup;
         this.tlsCertificateManager = tlsCertificateManager;
         serverBuilder = NettyServerBuilder.forPort(port)
             
.maxConcurrentCallsPerConnection(config.getGrpcMaxConcurrentCallsPerConnection());
@@ -60,30 +109,20 @@ public class GrpcServerBuilder {
         serverBuilder.protocolNegotiator(new ProxyAndTlsProtocolNegotiator());
 
         // build server
-        int bossLoopNum = config.getGrpcBossLoopNum();
-        int workerLoopNum = config.getGrpcWorkerLoopNum();
         int maxInboundMessageSize = config.getGrpcMaxInboundMessageSize();
         long idleTimeMills = config.getGrpcClientIdleTimeMills();
 
-        if (config.isEnableGrpcEpoll()) {
-            serverBuilder.bossEventLoopGroup(new 
EpollEventLoopGroup(bossLoopNum))
-                .workerEventLoopGroup(new EpollEventLoopGroup(workerLoopNum))
-                .channelType(EpollServerSocketChannel.class)
-                .executor(executor);
-        } else {
-            serverBuilder.bossEventLoopGroup(new 
NioEventLoopGroup(bossLoopNum))
-                .workerEventLoopGroup(new NioEventLoopGroup(workerLoopNum))
-                .channelType(NioServerSocketChannel.class)
-                .executor(executor);
-        }
+        serverBuilder.channelType(enableEpoll ? EpollServerSocketChannel.class 
: NioServerSocketChannel.class)
+            .executor(executor);
 
         serverBuilder.maxInboundMessageSize(maxInboundMessageSize)
             .maxConnectionIdle(idleTimeMills, TimeUnit.MILLISECONDS)
             
.permitKeepAliveTime(config.getGrpcServerPermitKeepAliveTimeMillis(), 
TimeUnit.MILLISECONDS)
             
.permitKeepAliveWithoutCalls(config.isGrpcServerPermitKeepAliveWithoutCalls());
 
-        log.info("grpc server has built. port: {}, bossLoopNum: {}, 
workerLoopNum: {}, maxInboundMessageSize: {}",
-            port, bossLoopNum, workerLoopNum, maxInboundMessageSize);
+        log.info("grpc server builder initialized. port: {}, bossLoopNum: {}, 
workerLoopNum: {}, "
+                + "callerOwnedEventLoops: {}, maxInboundMessageSize: {}",
+            port, bossLoopNum, workerLoopNum, bossGroup != null, 
maxInboundMessageSize);
     }
 
     public GrpcServerBuilder shutdownTime(long time, TimeUnit unit) {
@@ -108,7 +147,31 @@ public class GrpcServerBuilder {
     }
 
     public GrpcServer build() throws Exception {
-        return new GrpcServer(this.serverBuilder.build(), time, unit, 
tlsCertificateManager);
+        EventLoopGroup boss = bossGroup;
+        EventLoopGroup worker = workerGroup;
+        boolean ownsEventLoops = boss == null;
+        try {
+            if (ownsEventLoops) {
+                boss = newEventLoopGroup(bossLoopNum);
+                worker = newEventLoopGroup(workerLoopNum);
+            }
+            return new 
GrpcServer(serverBuilder.bossEventLoopGroup(boss).workerEventLoopGroup(worker).build(),
+                time, unit, tlsCertificateManager, ownsEventLoops ? boss : 
null, ownsEventLoops ? worker : null);
+        } catch (Exception | Error e) {
+            if (ownsEventLoops) {
+                if (boss != null) {
+                    boss.shutdownGracefully();
+                }
+                if (worker != null) {
+                    worker.shutdownGracefully();
+                }
+            }
+            throw e;
+        }
+    }
+
+    private EventLoopGroup newEventLoopGroup(int threads) {
+        return enableEpoll ? new EpollEventLoopGroup(threads) : new 
NioEventLoopGroup(threads);
     }
 
     public GrpcServerBuilder configInterceptor() {
diff --git 
a/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/GrpcServerTest.java 
b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/GrpcServerTest.java
new file mode 100644
index 0000000000..4e9624fc49
--- /dev/null
+++ b/proxy/src/test/java/org/apache/rocketmq/proxy/grpc/GrpcServerTest.java
@@ -0,0 +1,231 @@
+/*
+ * 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.grpc;
+
+import com.google.protobuf.StringValue;
+import io.grpc.CallOptions;
+import io.grpc.ManagedChannel;
+import io.grpc.MethodDescriptor;
+import io.grpc.Server;
+import io.grpc.ServerServiceDefinition;
+import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
+import io.grpc.netty.shaded.io.netty.channel.EventLoopGroup;
+import io.grpc.netty.shaded.io.netty.channel.MultithreadEventLoopGroup;
+import io.grpc.netty.shaded.io.netty.channel.epoll.Epoll;
+import io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoopGroup;
+import io.grpc.protobuf.ProtoUtils;
+import io.grpc.stub.ClientCalls;
+import io.grpc.stub.ServerCalls;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import org.apache.rocketmq.proxy.config.ConfigurationManager;
+import org.apache.rocketmq.proxy.config.InitConfigTest;
+import org.apache.rocketmq.proxy.config.ProxyConfig;
+import org.apache.rocketmq.proxy.service.cert.TlsCertificateManager;
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class GrpcServerTest extends InitConfigTest {
+    private final ThreadPoolExecutor executor = (ThreadPoolExecutor) 
Executors.newFixedThreadPool(2);
+    private final TlsCertificateManager certificates = 
mock(TlsCertificateManager.class);
+    private static final MethodDescriptor<StringValue, StringValue> METHOD =
+        MethodDescriptor.<StringValue, StringValue>newBuilder()
+            .setType(MethodDescriptor.MethodType.UNARY)
+            .setFullMethodName("test.Echo/Ping")
+            
.setRequestMarshaller(ProtoUtils.marshaller(StringValue.getDefaultInstance()))
+            
.setResponseMarshaller(ProtoUtils.marshaller(StringValue.getDefaultInstance()))
+            .build();
+
+    @Before
+    public void configure() {
+        ProxyConfig config = ConfigurationManager.getProxyConfig();
+        config.setEnableGrpcEpoll(false);
+        config.setTlsTestModeEnable(true);
+        config.setGrpcBossLoopNum(2);
+        config.setGrpcWorkerLoopNum(3);
+    }
+
+    @After
+    public void cleanup() {
+        executor.shutdownNow();
+    }
+
+    @Test
+    public void testDefaultOwnedGroupsTerminate() throws Exception {
+        checkOwnedGroups(GrpcServerBuilder.newBuilder(executor, 0, 
certificates), 2, 3);
+    }
+
+    @Test
+    public void testExplicitOwnedGroupCounts() throws Exception {
+        checkOwnedGroups(GrpcServerBuilder.newBuilder(executor, 0, 
certificates, 1, 1), 1, 1);
+    }
+
+    @Test
+    public void testEpollOwnedGroupsTerminate() throws Exception {
+        Assume.assumeTrue(Epoll.isAvailable());
+        ConfigurationManager.getProxyConfig().setEnableGrpcEpoll(true);
+        checkOwnedGroups(GrpcServerBuilder.newBuilder(executor, 0, 
certificates, 1, 1), 1, 1);
+    }
+
+    private void checkOwnedGroups(GrpcServerBuilder builder, int bossThreads, 
int workerThreads) throws Exception {
+        GrpcServer server = builder.addService(service()).build();
+        MultithreadEventLoopGroup boss = (MultithreadEventLoopGroup) 
field(server, "ownedBossGroup");
+        MultithreadEventLoopGroup worker = (MultithreadEventLoopGroup) 
field(server, "ownedWorkerGroup");
+        try {
+            assertThat(boss.executorCount()).isEqualTo(bossThreads);
+            assertThat(worker.executorCount()).isEqualTo(workerThreads);
+            server.start();
+            ping(server);
+        } finally {
+            server.shutdown();
+        }
+        assertTerminated(boss, worker);
+        verify(certificates).unregisterReloadListener(server.tlsReloadHandler);
+    }
+
+    @Test
+    public void testBorrowedGroupsSurviveShutdownAndBindFailure() throws 
Exception {
+        EventLoopGroup boss = new NioEventLoopGroup(1);
+        EventLoopGroup worker = new NioEventLoopGroup(1);
+        GrpcServer first = GrpcServerBuilder.newBuilder(executor, 0, 
certificates, boss, worker)
+            .addService(service()).build();
+        GrpcServer second = GrpcServerBuilder.newBuilder(executor, 0, 
certificates, boss, worker)
+            .addService(service()).build();
+        GrpcServer failed = null;
+        try {
+            first.start();
+            second.start();
+            ping(first);
+            ping(second);
+            first.shutdown();
+            ping(second);
+            assertThat(boss.isShuttingDown()).isFalse();
+            assertThat(worker.isShuttingDown()).isFalse();
+
+            failed = GrpcServerBuilder.newBuilder(executor, port(second), 
certificates, boss, worker).build();
+            assertThatThrownBy(failed::start).isInstanceOf(IOException.class);
+            ping(second);
+            assertThat(boss.isShuttingDown()).isFalse();
+            assertThat(worker.isShuttingDown()).isFalse();
+        } finally {
+            first.shutdown();
+            second.shutdown();
+            if (failed != null) {
+                failed.shutdown();
+            }
+            boss.shutdownGracefully();
+            worker.shutdownGracefully();
+        }
+        assertTerminated(boss, worker);
+    }
+
+    @Test
+    public void testOwnedGroupsTerminateAfterBindFailure() throws Exception {
+        GrpcServer running = GrpcServerBuilder.newBuilder(executor, 0, 
certificates, 1, 1).build();
+        GrpcServer failed = null;
+        try {
+            running.start();
+            failed = GrpcServerBuilder.newBuilder(executor, port(running), 
certificates, 1, 1).build();
+            assertThatThrownBy(failed::start).isInstanceOf(IOException.class);
+            assertTerminated((EventLoopGroup) field(failed, "ownedBossGroup"),
+                (EventLoopGroup) field(failed, "ownedWorkerGroup"));
+        } finally {
+            running.shutdown();
+            if (failed != null) {
+                failed.shutdown();
+            }
+        }
+    }
+
+    @Test
+    public void testShutdownTimeoutReleasesOwnedGroups() throws Exception {
+        Server delegate = mock(Server.class);
+        EventLoopGroup boss = mock(EventLoopGroup.class);
+        EventLoopGroup worker = mock(EventLoopGroup.class);
+        when(delegate.shutdown()).thenReturn(delegate);
+        when(delegate.shutdownNow()).thenReturn(delegate);
+        when(delegate.awaitTermination(1, TimeUnit.SECONDS)).thenReturn(false, 
true);
+        new GrpcServer(delegate, 1, TimeUnit.SECONDS, certificates, boss, 
worker).shutdown();
+        verify(delegate).shutdownNow();
+        verify(boss).shutdownGracefully();
+        verify(worker).shutdownGracefully();
+    }
+
+    @Test
+    public void testInterruptedShutdownReleasesOwnedGroups() throws Exception {
+        Server delegate = mock(Server.class);
+        EventLoopGroup boss = mock(EventLoopGroup.class);
+        EventLoopGroup worker = mock(EventLoopGroup.class);
+        when(delegate.shutdown()).thenReturn(delegate);
+        when(delegate.awaitTermination(1, TimeUnit.SECONDS)).thenThrow(new 
InterruptedException());
+        try {
+            new GrpcServer(delegate, 1, TimeUnit.SECONDS, certificates, boss, 
worker).shutdown();
+            assertThat(Thread.currentThread().isInterrupted()).isTrue();
+            verify(delegate).shutdownNow();
+            verify(boss).shutdownGracefully();
+            verify(worker).shutdownGracefully();
+        } finally {
+            Thread.interrupted();
+        }
+    }
+
+    private ServerServiceDefinition service() {
+        return ServerServiceDefinition.builder("test.Echo")
+            .addMethod(METHOD, ServerCalls.asyncUnaryCall((request, response) 
-> {
+                response.onNext(request);
+                response.onCompleted();
+            })).build();
+    }
+
+    private int port(GrpcServer server) throws Exception {
+        return ((Server) field(server, "server")).getPort();
+    }
+
+    private void ping(GrpcServer server) throws Exception {
+        ManagedChannel channel = NettyChannelBuilder.forAddress("127.0.0.1", 
port(server)).usePlaintext().build();
+        try {
+            assertThat(ClientCalls.blockingUnaryCall(channel, METHOD,
+                CallOptions.DEFAULT.withDeadlineAfter(5, TimeUnit.SECONDS), 
StringValue.of("ping")))
+                .isEqualTo(StringValue.of("ping"));
+        } finally {
+            channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);
+        }
+    }
+
+    private void assertTerminated(EventLoopGroup boss, EventLoopGroup worker) {
+        assertThat(boss.terminationFuture().awaitUninterruptibly(10, 
TimeUnit.SECONDS)).isTrue();
+        assertThat(worker.terminationFuture().awaitUninterruptibly(10, 
TimeUnit.SECONDS)).isTrue();
+    }
+
+    private Object field(GrpcServer server, String name) throws Exception {
+        Field field = GrpcServer.class.getDeclaredField(name);
+        field.setAccessible(true);
+        return field.get(server);
+    }
+}
diff --git 
a/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/PopPriorityIT.java
 
b/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/PopPriorityIT.java
index 9a9e67feac..c3e0f90a03 100644
--- 
a/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/PopPriorityIT.java
+++ 
b/test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/PopPriorityIT.java
@@ -20,6 +20,8 @@ package org.apache.rocketmq.test.client.consumer.pop;
 import org.apache.rocketmq.client.consumer.PopResult;
 import org.apache.rocketmq.client.consumer.PopStatus;
 import org.apache.rocketmq.client.producer.SendResult;
+import org.apache.rocketmq.common.KeyBuilder;
+import org.apache.rocketmq.common.TopicConfig;
 import org.apache.rocketmq.common.attribute.AttributeParser;
 import org.apache.rocketmq.common.attribute.CQType;
 import org.apache.rocketmq.common.attribute.TopicMessageType;
@@ -38,15 +40,16 @@ import org.junit.runners.Parameterized;
 
 import java.time.Duration;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Random;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 import static 
org.apache.rocketmq.common.SubscriptionGroupAttributes.PRIORITY_FACTOR_ATTRIBUTE;
 import static org.awaitility.Awaitility.await;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 @RunWith(Parameterized.class)
@@ -61,7 +64,7 @@ public class PopPriorityIT extends BasePopNormally {
         this.priorityOrderAsc = priorityOrderAsc;
     }
 
-    @Parameterized.Parameters
+    @Parameterized.Parameters(name = "kv={0}, ascending={1}")
     public static List<Object[]> params() {
         List<Object[]> result = new ArrayList<>();
         result.add(new Object[] {false, true});
@@ -120,10 +123,9 @@ public class PopPriorityIT extends BasePopNormally {
                 producer.send(message);
             }
         }
-        Assert.assertTrue(awaitDispatchMs(2000));
+        awaitStoredMessages(topic, writeQueueNum * msgNumPerQueue);
         for (int i = 0; i < msgNumPerQueue; i++) {
-            PopResult popResult = 
popMessageAsync(Duration.ofSeconds(600).toMillis(), 1, 30000).get();
-            TestUtil.waitForMonment(20); // wait lock release
+            PopResult popResult = 
popMessages(Duration.ofSeconds(600).toMillis(), 1, 30000);
             assertEquals(PopStatus.FOUND, popResult.getPopStatus());
             MessageExt message = popResult.getMsgFoundList().get(0);
             assertEquals(maxPriority, message.getPriority()); // not a 
coincidence
@@ -136,9 +138,9 @@ public class PopPriorityIT extends BasePopNormally {
             Message message = mockMessage(topic, i, String.valueOf(i));
             producer.send(message);
         }
-        Assert.assertTrue(awaitDispatchMs(2000));
+        awaitStoredMessages(topic, writeQueueNum);
         for (int i = 0; i < writeQueueNum; i++) {
-            PopResult popResult = 
popMessageAsync(Duration.ofSeconds(30).toMillis(), 1, 30000).get();
+            PopResult popResult = 
popMessages(Duration.ofSeconds(600).toMillis(), 1, 30000);
             assertEquals(PopStatus.FOUND, popResult.getPopStatus());
             MessageExt message = popResult.getMsgFoundList().get(0);
             int expectPriority = priorityOrderAsc ? writeQueueNum - 1 - i : i;
@@ -162,12 +164,11 @@ public class PopPriorityIT extends BasePopNormally {
                 producer.send(message);
             }
         }
-        Assert.assertTrue(awaitDispatchMs(2000));
+        awaitStoredMessages(topic, writeQueueNum * msgNumPerQueue);
         int sampleCount = 800;
         int[] queueIdCount = new int[writeQueueNum];
         for (int i = 0; i < sampleCount; i++) {
-            PopResult popResult = 
popMessageAsync(Duration.ofSeconds(600).toMillis(), 1, 30000).get();
-            TestUtil.waitForMonment(10); // wait lock release
+            PopResult popResult = 
popMessages(Duration.ofSeconds(600).toMillis(), 1, 30000);
             assertEquals(PopStatus.FOUND, popResult.getPopStatus());
             MessageExt message = popResult.getMsgFoundList().get(0);
             queueIdCount[message.getQueueId()] = 
queueIdCount[message.getQueueId()] + 1;
@@ -175,7 +176,8 @@ public class PopPriorityIT extends BasePopNormally {
 
         double expectAverage = (double) sampleCount / writeQueueNum;
         for (int count : queueIdCount) {
-            assertTrue(Math.abs(count - expectAverage) < expectAverage * 0.4);
+            assertTrue("Unexpected queue distribution: " + 
Arrays.toString(queueIdCount),
+                Math.abs(count - expectAverage) < expectAverage * 0.4);
         }
     }
 
@@ -184,22 +186,22 @@ public class PopPriorityIT extends BasePopNormally {
         // retry as lowest by default
         int count = 100;
         for (int i = 0; i < count; i++) {
-            Message message = mockMessage(topic, new 
Random().nextInt(writeQueueNum), String.valueOf(i));
+            Message message = mockMessage(topic, i % writeQueueNum, 
String.valueOf(i));
             producer.send(message);
         }
+        awaitStoredMessages(topic, count);
         int invisibleTime = 3;
-        PopResult popResult = 
popMessageAsync(Duration.ofSeconds(invisibleTime).toMillis(), 1, 30000).get();
+        PopResult popResult = 
popMessages(Duration.ofSeconds(invisibleTime).toMillis(), 1, 30000);
         assertEquals(PopStatus.FOUND, popResult.getPopStatus());
         String retryId = popResult.getMsgFoundList().get(0).getMsgId();
-        TestUtil.waitForSeconds(invisibleTime + 3);
-        Assert.assertTrue(awaitDispatchMs(2000));
+        awaitRetryMessages(1);
 
         List<MessageExt> collect = new ArrayList<>();
         await()
             .pollInterval(1, TimeUnit.SECONDS)
             .atMost(35, TimeUnit.SECONDS)
             .until(() -> {
-                PopResult result = 
popMessageAsync(Duration.ofSeconds(600).toMillis(), 32, 5000).get();
+                PopResult result = 
popMessages(Duration.ofSeconds(600).toMillis(), 32, 5000);
                 if (PopStatus.FOUND.equals(result.getPopStatus())) {
                     collect.addAll(result.getMsgFoundList());
                 }
@@ -215,22 +217,22 @@ public class PopPriorityIT extends BasePopNormally {
         
brokerController1.getBrokerConfig().setPopFromRetryProbabilityForPriority(100);
         int count = 100;
         for (int i = 0; i < count; i++) {
-            Message message = mockMessage(topic, new 
Random().nextInt(writeQueueNum), String.valueOf(i));
+            Message message = mockMessage(topic, i % writeQueueNum, 
String.valueOf(i));
             producer.send(message);
         }
+        awaitStoredMessages(topic, count);
         int invisibleTime = 3;
-        PopResult popResult = 
popMessageAsync(Duration.ofSeconds(invisibleTime).toMillis(), 1, 30000).get();
+        PopResult popResult = 
popMessages(Duration.ofSeconds(invisibleTime).toMillis(), 1, 30000);
         assertEquals(PopStatus.FOUND, popResult.getPopStatus());
         String retryId = popResult.getMsgFoundList().get(0).getMsgId();
-        TestUtil.waitForSeconds(invisibleTime + 3);
-        Assert.assertTrue(awaitDispatchMs(2000));
+        awaitRetryMessages(1);
 
         List<MessageExt> collect = new ArrayList<>();
         await()
             .pollInterval(1, TimeUnit.SECONDS)
             .atMost(35, TimeUnit.SECONDS)
             .until(() -> {
-                PopResult result = 
popMessageAsync(Duration.ofSeconds(600).toMillis(), 32, 5000).get();
+                PopResult result = 
popMessages(Duration.ofSeconds(600).toMillis(), 32, 5000);
                 if (PopStatus.FOUND.equals(result.getPopStatus())) {
                     collect.addAll(result.getMsgFoundList());
                 }
@@ -249,19 +251,19 @@ public class PopPriorityIT extends BasePopNormally {
             Message message = mockMessage(topic, i, String.valueOf(i));
             producer.send(message);
         }
-        Assert.assertTrue(awaitDispatchMs(2000));
+        awaitStoredMessages(topic, writeQueueNum);
         int invisibleTime = 3;
-        PopResult popResult = 
popMessageAsync(Duration.ofSeconds(invisibleTime).toMillis(), writeQueueNum, 
30000).get();
+        PopResult popResult = 
popMessages(Duration.ofSeconds(invisibleTime).toMillis(), writeQueueNum, 30000);
         assertEquals(PopStatus.FOUND, popResult.getPopStatus());
         assertEquals(writeQueueNum, popResult.getMsgFoundList().size());
-        TestUtil.waitForSeconds(invisibleTime + 3);
+        awaitRetryMessages(writeQueueNum);
 
         List<MessageExt> collect = new ArrayList<>();
         await()
             .pollInterval(1, TimeUnit.SECONDS)
             .atMost(35, TimeUnit.SECONDS)
             .until(() -> {
-                PopResult result = 
popMessageAsync(Duration.ofSeconds(600).toMillis(), 32, 5000).get();
+                PopResult result = 
popMessages(Duration.ofSeconds(600).toMillis(), 32, 5000);
                 if (PopStatus.FOUND.equals(result.getPopStatus())) {
                     collect.addAll(result.getMsgFoundList());
                 }
@@ -315,6 +317,53 @@ public class PopPriorityIT extends BasePopNormally {
         assertEquals(0, msgList.get(msgList.size() - 1).getQueueOffset()); // 
means a separate retry queue
     }
 
+    private void awaitStoredMessages(String storedTopic, int expectedCount) {
+        await().alias("consume queues for " + storedTopic)
+            .pollInterval(10, TimeUnit.MILLISECONDS)
+            .atMost(30, TimeUnit.SECONDS)
+            .untilAsserted(() -> {
+                TopicConfig topicConfig = 
brokerController1.getTopicConfigManager().selectTopicConfig(storedTopic);
+                assertNotNull("Topic has not been created: " + storedTopic, 
topicConfig);
+                long storedCount = 0;
+                for (int queueId = 0; queueId < 
topicConfig.getReadQueueNums(); queueId++) {
+                    storedCount += 
brokerController1.getMessageStore().getMaxOffsetInQueue(storedTopic, queueId);
+                }
+                assertEquals("Messages dispatched to " + storedTopic, 
expectedCount, storedCount);
+            });
+    }
+
+    private void awaitRetryMessages(int expectedCount) {
+        // Expiration alone does not mean the retry has been revived and 
dispatched to its consume queue.
+        String retryTopic = KeyBuilder.buildPopRetryTopic(topic, group,
+            brokerController1.getBrokerConfig().isEnableRetryTopicV2());
+        awaitStoredMessages(retryTopic, expectedCount);
+    }
+
+    private PopResult popMessages(long invisibleTime, int maxNums, long 
timeout) throws Exception {
+        // A response can reach the client before the previous request's 
queue-lock completion callback runs.
+        await().alias("previous POP locks released")
+            .pollDelay(0, TimeUnit.MILLISECONDS)
+            .pollInterval(10, TimeUnit.MILLISECONDS)
+            .atMost(10, TimeUnit.SECONDS)
+            .until(() -> {
+                if (popConsumerKVServiceEnable) {
+                    if 
(!brokerController1.getPopConsumerService().getConsumerLockService().tryLock(group,
 topic)) {
+                        return false;
+                    }
+                    
brokerController1.getPopConsumerService().getConsumerLockService().unlock(group,
 topic);
+                } else {
+                    for (int queueId = 0; queueId < writeQueueNum; queueId++) {
+                        if 
(!brokerController1.getPopMessageProcessor().getQueueLockManager().tryLock(topic,
 group, queueId)) {
+                            return false;
+                        }
+                        
brokerController1.getPopMessageProcessor().getQueueLockManager().unLock(topic, 
group, queueId);
+                    }
+                }
+                return true;
+            });
+        return popMessageAsync(invisibleTime, maxNums, timeout).get();
+    }
+
     private static Message mockMessage(String topic, int priority, String key) 
{
         Message msg = new Message(topic, "HW".getBytes());
         if (priority >= 0) {

Reply via email to