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 fd0c95920e [ISSUE #10827] fix(broker): spin for the lock on 
same-attemptId pop orderly retry to avoid empty response (#10828)
fd0c95920e is described below

commit fd0c95920e0deac96ce2ae27442747cc5e65e930
Author: lizhimins <[email protected]>
AuthorDate: Fri Aug 7 18:29:26 2026 +0800

    [ISSUE #10827] fix(broker): spin for the lock on same-attemptId pop orderly 
retry to avoid empty response (#10828)
---
 .../rocketmq/broker/pop/PopConsumerService.java    |  28 +++-
 .../pop/orderly/ConsumerOrderInfoManager.java      |  12 ++
 .../pop/orderly/QueueLevelConsumerManager.java     |  14 ++
 .../pop/PopConsumerServiceLockRetryTest.java       | 158 +++++++++++++++++++++
 .../pop/orderly/ConsumerOrderInfoManagerTest.java  |  22 +++
 5 files changed, 233 insertions(+), 1 deletion(-)

diff --git 
a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java 
b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java
index 9ab5eb651b..f72e2ba26f 100644
--- 
a/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java
+++ 
b/broker/src/main/java/org/apache/rocketmq/broker/pop/PopConsumerService.java
@@ -360,7 +360,7 @@ public class PopConsumerService extends ServiceThread {
             new PopConsumerContext(clientHost, popTime, invisibleTime, 
groupId, fifo, initMode, attemptId);
 
         TopicConfig topicConfig = 
brokerController.getTopicConfigManager().selectTopicConfig(topicId);
-        if (topicConfig == null || !consumerLockService.tryLock(groupId, 
topicId)) {
+        if (topicConfig == null || !this.tryLockForPop(groupId, topicId, fifo, 
attemptId)) {
             return CompletableFuture.completedFuture(popConsumerContext);
         }
 
@@ -470,6 +470,32 @@ public class PopConsumerService extends ServiceThread {
         return getMessageFuture;
     }
 
+    /**
+     * Fifo pops carrying an attemptId already registered in OrderInfo are 
in-flight retries
+     * of the same receive attempt. Instead of failing fast on lock contention 
(which leaves
+     * the retry empty and burns the reentrant attemptId), keep retrying the 
lock; pops with
+     * a different attemptId would be blocked by checkBlock anyway, so they 
keep the
+     * fail-fast behavior.
+     */
+    private boolean tryLockForPop(String groupId, String topicId, boolean 
fifo, String attemptId) {
+        if (consumerLockService.tryLock(groupId, topicId)) {
+            return true;
+        }
+        if (!fifo || attemptId == null || attemptId.isEmpty()
+            || 
!brokerController.getConsumerOrderInfoManager().isAttemptIdMatched(attemptId, 
topicId, groupId)) {
+            return false;
+        }
+        // The lock holder always releases on pop completion, and stale locks 
are
+        // removed by PopConsumerLockService.removeTimeout(), so keep retrying 
until
+        // the lock is acquired to make sure the in-flight retry is not left 
empty.
+        // Same-attemptId contention is rare and the wait is normally 
milliseconds,
+        // so a plain spin is fine here.
+        while (!consumerLockService.tryLock(groupId, topicId)) {
+            Thread.yield();
+        }
+        return true;
+    }
+
     // Notify polling request when receive orderly ack
     public CompletableFuture<Boolean> ackAsync(
         long popTime, long invisibleTime, String groupId, String topicId, int 
queueId, long offset) {
diff --git 
a/broker/src/main/java/org/apache/rocketmq/broker/pop/orderly/ConsumerOrderInfoManager.java
 
b/broker/src/main/java/org/apache/rocketmq/broker/pop/orderly/ConsumerOrderInfoManager.java
index 84b0540db2..632c02053e 100644
--- 
a/broker/src/main/java/org/apache/rocketmq/broker/pop/orderly/ConsumerOrderInfoManager.java
+++ 
b/broker/src/main/java/org/apache/rocketmq/broker/pop/orderly/ConsumerOrderInfoManager.java
@@ -68,6 +68,18 @@ public interface ConsumerOrderInfoManager {
      */
     boolean checkBlock(String attemptId, String topic, String group, int 
queueId, long invisibleTime);
 
+    /**
+     * Check whether the given attemptId has already been registered in the 
order info of
+     * the topic and group, i.e. the request is an in-flight retry of a 
previous delivery
+     * of the same receive attempt
+     *
+     * @param attemptId Attempt ID
+     * @param topic     Topic name
+     * @param group     Consumer group name
+     * @return true indicates the attemptId is registered in some queue's 
order info
+     */
+    boolean isAttemptIdMatched(String attemptId, String topic, String group);
+
     /**
      * Remove the specified topic and group
      * Usually called during topic deletion
diff --git 
a/broker/src/main/java/org/apache/rocketmq/broker/pop/orderly/QueueLevelConsumerManager.java
 
b/broker/src/main/java/org/apache/rocketmq/broker/pop/orderly/QueueLevelConsumerManager.java
index 6f496fa13b..d1f8008a40 100644
--- 
a/broker/src/main/java/org/apache/rocketmq/broker/pop/orderly/QueueLevelConsumerManager.java
+++ 
b/broker/src/main/java/org/apache/rocketmq/broker/pop/orderly/QueueLevelConsumerManager.java
@@ -171,6 +171,20 @@ public class QueueLevelConsumerManager extends 
ConfigManager implements Consumer
         return orderInfo.needBlock(attemptId, invisibleTime);
     }
 
+    @Override
+    public boolean isAttemptIdMatched(String attemptId, String topic, String 
group) {
+        ConcurrentHashMap<Integer/*queueId*/, OrderInfo> qs = 
table.get(buildKey(topic, group));
+        if (qs == null || attemptId == null) {
+            return false;
+        }
+        for (OrderInfo orderInfo : qs.values()) {
+            if (orderInfo != null && 
attemptId.equals(orderInfo.getAttemptId())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     @Override
     public void clearBlock(String topic, String group, int queueId) {
         table.computeIfPresent(buildKey(topic, group), (key, val) -> {
diff --git 
a/broker/src/test/java/org/apache/rocketmq/broker/pop/PopConsumerServiceLockRetryTest.java
 
b/broker/src/test/java/org/apache/rocketmq/broker/pop/PopConsumerServiceLockRetryTest.java
new file mode 100644
index 0000000000..7b9f37505b
--- /dev/null
+++ 
b/broker/src/test/java/org/apache/rocketmq/broker/pop/PopConsumerServiceLockRetryTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.broker.pop;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.rocketmq.broker.BrokerController;
+import org.apache.rocketmq.broker.offset.ConsumerOffsetManager;
+import org.apache.rocketmq.broker.pop.orderly.ConsumerOrderInfoManager;
+import org.apache.rocketmq.broker.subscription.SubscriptionGroupManager;
+import org.apache.rocketmq.broker.topic.TopicConfigManager;
+import org.apache.rocketmq.common.BrokerConfig;
+import org.apache.rocketmq.common.TopicConfig;
+import org.apache.rocketmq.common.constant.ConsumeInitMode;
+import 
org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig;
+import org.apache.rocketmq.store.GetMessageResult;
+import org.apache.rocketmq.store.GetMessageStatus;
+import org.apache.rocketmq.store.MessageStore;
+import org.apache.rocketmq.store.config.MessageStoreConfig;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class PopConsumerServiceLockRetryTest {
+
+    private static final String GROUP_ID = "groupId";
+    private static final String TOPIC_ID = "topicId";
+    private static final String ATTEMPT_ID = "attempt-id-lock-retry";
+    private static final long INVISIBLE_TIME = 300_000L;
+
+    private final String filePath = 
PopConsumerRocksdbStoreTest.getRandomStorePath();
+
+    private BrokerController brokerController;
+    private PopConsumerLockService consumerLockService;
+    private SubscriptionGroupManager subscriptionGroupManager;
+    private ConsumerOffsetManager consumerOffsetManager;
+    private ConsumerOrderInfoManager consumerOrderInfoManager;
+    private MessageStore messageStore;
+    private PopConsumerService consumerService;
+
+    @Before
+    public void init() throws IOException, IllegalAccessException {
+        BrokerConfig brokerConfig = new BrokerConfig();
+        MessageStoreConfig messageStoreConfig = new MessageStoreConfig();
+        messageStoreConfig.setStorePathRootDir(filePath);
+
+        TopicConfigManager topicConfigManager = 
Mockito.mock(TopicConfigManager.class);
+        subscriptionGroupManager = 
Mockito.mock(SubscriptionGroupManager.class);
+        consumerOffsetManager = Mockito.mock(ConsumerOffsetManager.class);
+        consumerOrderInfoManager = 
Mockito.mock(ConsumerOrderInfoManager.class);
+        consumerLockService = Mockito.mock(PopConsumerLockService.class);
+        messageStore = Mockito.mock(MessageStore.class);
+
+        brokerController = Mockito.mock(BrokerController.class);
+        
Mockito.when(brokerController.getBrokerConfig()).thenReturn(brokerConfig);
+        
Mockito.when(brokerController.getMessageStoreConfig()).thenReturn(messageStoreConfig);
+        
Mockito.when(brokerController.getTopicConfigManager()).thenReturn(topicConfigManager);
+        
Mockito.when(brokerController.getSubscriptionGroupManager()).thenReturn(subscriptionGroupManager);
+        
Mockito.when(brokerController.getConsumerOffsetManager()).thenReturn(consumerOffsetManager);
+        
Mockito.when(brokerController.getConsumerOrderInfoManager()).thenReturn(consumerOrderInfoManager);
+        
Mockito.when(brokerController.getMessageStore()).thenReturn(messageStore);
+        Mockito.when(topicConfigManager.selectTopicConfig(Mockito.anyString()))
+            .thenReturn(new TopicConfig(TOPIC_ID));
+
+        consumerService = new PopConsumerService(brokerController);
+        // the lock service is built inside the constructor, replace it for 
verification
+        FieldUtils.writeField(consumerService, "consumerLockService", 
consumerLockService, true);
+    }
+
+    @After
+    public void shutdown() throws IOException {
+        FileUtils.deleteDirectory(new File(filePath));
+    }
+
+    private void stubEmptyStore() {
+        GetMessageResult result = new GetMessageResult();
+        result.setStatus(GetMessageStatus.NO_MESSAGE_IN_QUEUE);
+        result.setNextBeginOffset(0L);
+        Mockito.when(messageStore.getMessageAsync(Mockito.anyString(), 
Mockito.anyString(),
+                Mockito.anyInt(), Mockito.anyLong(), Mockito.anyInt(), 
Mockito.any()))
+            .thenReturn(CompletableFuture.completedFuture(result));
+        Mockito.when(consumerOffsetManager.queryOffset(Mockito.anyString(), 
Mockito.anyString(),
+            Mockito.anyInt())).thenReturn(0L);
+    }
+
+    @Test
+    public void popAsyncOrderlyLockRetryPersistsTest() {
+        // the retry keeps spinning until the lock holder releases, no early 
give-up
+        AtomicInteger attempts = new AtomicInteger();
+        Mockito.when(consumerLockService.tryLock(Mockito.anyString(), 
Mockito.anyString()))
+            .thenAnswer(invocation -> attempts.incrementAndGet() > 50);
+        Mockito.when(consumerOrderInfoManager.isAttemptIdMatched(ATTEMPT_ID, 
TOPIC_ID, GROUP_ID))
+            .thenReturn(true);
+        
Mockito.when(subscriptionGroupManager.findSubscriptionGroupConfig(Mockito.anyString()))
+            .thenReturn(new SubscriptionGroupConfig());
+        stubEmptyStore();
+
+        PopConsumerContext result = consumerService.popAsync("127.0.0.1", 
System.currentTimeMillis(),
+            INVISIBLE_TIME, GROUP_ID, TOPIC_ID, 0, 32, true, ATTEMPT_ID, 
ConsumeInitMode.MIN, null).join();
+
+        assertNotNull(result);
+        Mockito.verify(consumerLockService, 
Mockito.times(51)).tryLock(Mockito.anyString(), Mockito.anyString());
+        
Mockito.verify(subscriptionGroupManager).findSubscriptionGroupConfig(GROUP_ID);
+    }
+
+    @Test
+    public void popAsyncUnregisteredAttemptIdFailFastTest() {
+        // a fifo pop whose attemptId is not registered in OrderInfo is not an 
in-flight
+        // retry of a previous delivery, so it must keep the fail-fast behavior
+        Mockito.when(consumerLockService.tryLock(Mockito.anyString(), 
Mockito.anyString()))
+            .thenReturn(false);
+        Mockito.when(consumerOrderInfoManager.isAttemptIdMatched(ATTEMPT_ID, 
TOPIC_ID, GROUP_ID))
+            .thenReturn(false);
+
+        PopConsumerContext result = consumerService.popAsync("127.0.0.1", 
System.currentTimeMillis(),
+            INVISIBLE_TIME, GROUP_ID, TOPIC_ID, 0, 32, true, ATTEMPT_ID, 
ConsumeInitMode.MIN, null).join();
+
+        assertNotNull(result);
+        assertEquals(0, result.getMessageCount());
+        Mockito.verify(consumerLockService, 
Mockito.times(1)).tryLock(Mockito.anyString(), Mockito.anyString());
+    }
+
+    @Test
+    public void popAsyncNonFifoFailFastTest() {
+        Mockito.when(consumerLockService.tryLock(Mockito.anyString(), 
Mockito.anyString()))
+            .thenReturn(false);
+
+        PopConsumerContext result = consumerService.popAsync("127.0.0.1", 
System.currentTimeMillis(),
+            INVISIBLE_TIME, GROUP_ID, TOPIC_ID, 0, 32, false, null, 
ConsumeInitMode.MIN, null).join();
+
+        assertNotNull(result);
+        assertEquals(0, result.getMessageCount());
+        // non-fifo requests must keep the fail-fast behavior
+        Mockito.verify(consumerLockService, 
Mockito.times(1)).tryLock(Mockito.anyString(), Mockito.anyString());
+    }
+}
diff --git 
a/broker/src/test/java/org/apache/rocketmq/broker/pop/orderly/ConsumerOrderInfoManagerTest.java
 
b/broker/src/test/java/org/apache/rocketmq/broker/pop/orderly/ConsumerOrderInfoManagerTest.java
index a5a5dfc235..557c861bd2 100644
--- 
a/broker/src/test/java/org/apache/rocketmq/broker/pop/orderly/ConsumerOrderInfoManagerTest.java
+++ 
b/broker/src/test/java/org/apache/rocketmq/broker/pop/orderly/ConsumerOrderInfoManagerTest.java
@@ -535,6 +535,28 @@ public class ConsumerOrderInfoManagerTest {
         assertFalse(consumerOrderInfoManager.checkBlock(attemptId, TOPIC, 
GROUP, QUEUE_ID_0, 3000));
     }
 
+    @Test
+    public void isAttemptIdMatchTest() {
+        StringBuilder orderInfoBuilder = new StringBuilder();
+        String attemptId = UUID.randomUUID().toString();
+        consumerOrderInfoManager.update(
+            attemptId,
+            false,
+            TOPIC,
+            GROUP,
+            QUEUE_ID_0,
+            popTime,
+            3000,
+            Lists.newArrayList(1L, 2L, 3L),
+            orderInfoBuilder
+        );
+
+        assertTrue(consumerOrderInfoManager.isAttemptIdMatched(attemptId, 
TOPIC, GROUP));
+        
assertFalse(consumerOrderInfoManager.isAttemptIdMatched(UUID.randomUUID().toString(),
 TOPIC, GROUP));
+        assertFalse(consumerOrderInfoManager.isAttemptIdMatched(attemptId, 
"unknownTopic", GROUP));
+        assertFalse(consumerOrderInfoManager.isAttemptIdMatched(null, TOPIC, 
GROUP));
+    }
+
     @Test
     public void testGetMaxLockFreeTimestamp() {
         QueueLevelConsumerManager.OrderInfo orderInfo = new 
QueueLevelConsumerManager.OrderInfo();

Reply via email to