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

RongtongJin 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 bf5d874770 [ISSUE #11041] Fix off-by-one in paginated metadata sync 
dropping the last entry (#11042)
bf5d874770 is described below

commit bf5d874770f709e2f42c008756cbcd1cb2ed0912
Author: 烤化の初雪 <[email protected]>
AuthorDate: Tue Oct 27 11:08:42 2026 +0800

    [ISSUE #11041] Fix off-by-one in paginated metadata sync dropping the last 
entry (#11042)
    
    The split-metadata pagination loops in MQClientAPIImpl and
    BrokerOuterAPI terminated on seq >= totalNum - 1 while the broker pages
    [seq, seq + maxNum) without overlap. For any entry count equal to one
    over a page boundary (N % pageSize == 1) the last topic/subscription
    group was silently never requested. SlaveSynchronize#syncTopicConfig
    uses the same loop, so slaves permanently missed the newest topic.
    Terminate on seq >= totalNum instead.
    
    Co-authored-by: root <[email protected]>
---
 .../apache/rocketmq/broker/out/BrokerOuterAPI.java |  4 +-
 .../rocketmq/client/impl/MQClientAPIImpl.java      |  4 +-
 .../rocketmq/client/impl/MQClientAPIImplTest.java  | 96 ++++++++++++++++++++++
 3 files changed, 100 insertions(+), 4 deletions(-)

diff --git 
a/broker/src/main/java/org/apache/rocketmq/broker/out/BrokerOuterAPI.java 
b/broker/src/main/java/org/apache/rocketmq/broker/out/BrokerOuterAPI.java
index ba4ba2ccf9..fb44e8e223 100644
--- a/broker/src/main/java/org/apache/rocketmq/broker/out/BrokerOuterAPI.java
+++ b/broker/src/main/java/org/apache/rocketmq/broker/out/BrokerOuterAPI.java
@@ -831,7 +831,7 @@ public class BrokerOuterAPI {
                     continue;
                 }
 
-                if (topicSeq >= totalTopicNum - 1) {
+                if (topicSeq >= totalTopicNum) {
                     LOGGER.info("get all topic config, totalTopicNum: {}", 
totalTopicNum);
                     break;
                 }
@@ -981,7 +981,7 @@ public class BrokerOuterAPI {
                     continue;
                 }
 
-                if (groupSeq >= totalGroupNum - 1) {
+                if (groupSeq >= totalGroupNum) {
                     LOGGER.info("get all subscription group config, 
totalGroupNum: {}", totalGroupNum);
                     break;
                 }
diff --git 
a/client/src/main/java/org/apache/rocketmq/client/impl/MQClientAPIImpl.java 
b/client/src/main/java/org/apache/rocketmq/client/impl/MQClientAPIImpl.java
index 6999859491..d921bc554f 100644
--- a/client/src/main/java/org/apache/rocketmq/client/impl/MQClientAPIImpl.java
+++ b/client/src/main/java/org/apache/rocketmq/client/impl/MQClientAPIImpl.java
@@ -3030,7 +3030,7 @@ public class MQClientAPIImpl implements 
NameServerUpdateCallback, StartAndShutdo
                     continue;
                 }
 
-                if (groupSeq >= totalGroupNum - 1) {
+                if (groupSeq >= totalGroupNum) {
                     log.info("get all subscription group config, 
totalGroupNum: {}", totalGroupNum);
                     break;
                 }
@@ -3122,7 +3122,7 @@ public class MQClientAPIImpl implements 
NameServerUpdateCallback, StartAndShutdo
                     continue;
                 }
 
-                if (topicSeq >= totalTopicNum - 1) {
+                if (topicSeq >= totalTopicNum) {
                     log.info("get all topic config, totalTopicNum: {}", 
totalTopicNum);
                     break;
                 }
diff --git 
a/client/src/test/java/org/apache/rocketmq/client/impl/MQClientAPIImplTest.java 
b/client/src/test/java/org/apache/rocketmq/client/impl/MQClientAPIImplTest.java
index 096853c6e6..1439ec4e96 100644
--- 
a/client/src/test/java/org/apache/rocketmq/client/impl/MQClientAPIImplTest.java
+++ 
b/client/src/test/java/org/apache/rocketmq/client/impl/MQClientAPIImplTest.java
@@ -65,6 +65,7 @@ import org.apache.rocketmq.remoting.netty.NettyRemotingClient;
 import org.apache.rocketmq.remoting.netty.ResponseFuture;
 import org.apache.rocketmq.remoting.protocol.RemotingCommand;
 import org.apache.rocketmq.remoting.protocol.RemotingSerializable;
+import org.apache.rocketmq.remoting.protocol.DataVersion;
 import org.apache.rocketmq.remoting.protocol.RequestCode;
 import org.apache.rocketmq.remoting.protocol.ResponseCode;
 import org.apache.rocketmq.remoting.protocol.admin.ConsumeStats;
@@ -110,6 +111,8 @@ import 
org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader;
 import org.apache.rocketmq.remoting.protocol.header.ExtraInfoUtil;
 import 
org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupResponseBody;
 import 
org.apache.rocketmq.remoting.protocol.header.GetConsumerListByGroupResponseHeader;
+import 
org.apache.rocketmq.remoting.protocol.header.GetAllSubscriptionGroupResponseHeader;
+import 
org.apache.rocketmq.remoting.protocol.header.GetAllTopicConfigResponseHeader;
 import 
org.apache.rocketmq.remoting.protocol.header.GetEarliestMsgStoretimeResponseHeader;
 import org.apache.rocketmq.remoting.protocol.header.GetMaxOffsetResponseHeader;
 import org.apache.rocketmq.remoting.protocol.header.GetMinOffsetResponseHeader;
@@ -156,6 +159,7 @@ import org.mockito.junit.MockitoJUnitRunner;
 import org.mockito.stubbing.Answer;
 
 import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
 import java.lang.reflect.Field;
 import java.net.InetSocketAddress;
 import java.util.ArrayList;
@@ -1740,6 +1744,88 @@ public class MQClientAPIImplTest {
         assertEquals(0, actual.getDataVersion().getStateVersion());
     }
 
+    @Test
+    public void 
testGetAllTopicConfigFetchesLastEntryWhenTotalIsOneOverPageSize()
+        throws Exception {
+        final int pageSize = 100;
+        final int total = pageSize + 1;
+        ClientConfig pagedClientConfig = new ClientConfig();
+        pagedClientConfig.setMaxPageSizeInGetMetadata(pageSize);
+        MQClientAPIImpl pagedApi = new MQClientAPIImpl(new 
NettyClientConfig(), null, null, pagedClientConfig);
+        Field field = MQClientAPIImpl.class.getDeclaredField("remotingClient");
+        field.setAccessible(true);
+        field.set(pagedApi, remotingClient);
+
+        DataVersion dataVersion = new DataVersion();
+        TopicConfigSerializeWrapper page1 = new TopicConfigSerializeWrapper();
+        for (int i = 0; i < pageSize; i++) {
+            page1.getTopicConfigTable().put("topic-" + i, new 
TopicConfig("topic-" + i));
+        }
+        page1.setDataVersion(dataVersion);
+        TopicConfigSerializeWrapper page2 = new TopicConfigSerializeWrapper();
+        page2.getTopicConfigTable().put("topic-" + pageSize, new 
TopicConfig("topic-" + pageSize));
+        page2.setDataVersion(dataVersion);
+
+        GetAllTopicConfigResponseHeader header1 = new 
GetAllTopicConfigResponseHeader();
+        header1.setTotalTopicNum(total);
+        RemotingCommand resp1 = 
wireResponse(RemotingCommand.createResponseCommandWithHeader(ResponseCode.SUCCESS,
 header1));
+        resp1.setBody(RemotingSerializable.encode(page1));
+        GetAllTopicConfigResponseHeader header2 = new 
GetAllTopicConfigResponseHeader();
+        header2.setTotalTopicNum(total);
+        RemotingCommand resp2 = 
wireResponse(RemotingCommand.createResponseCommandWithHeader(ResponseCode.SUCCESS,
 header2));
+        resp2.setBody(RemotingSerializable.encode(page2));
+        when(remotingClient.invokeSync(anyString(), 
any(RemotingCommand.class), anyLong()))
+            .thenReturn(resp1, resp2);
+
+        TopicConfigSerializeWrapper actual = 
pagedApi.getAllTopicConfig(defaultBrokerAddr, defaultTimeout);
+
+        assertEquals(total, actual.getTopicConfigTable().size());
+        assertEquals("topic-" + pageSize, 
actual.getTopicConfigTable().get("topic-" + pageSize).getTopicName());
+    }
+
+    @Test
+    public void 
testGetAllSubscriptionGroupFetchesLastEntryWhenTotalIsOneOverPageSize()
+        throws Exception {
+        final int pageSize = 100;
+        final int total = pageSize + 1;
+        ClientConfig pagedClientConfig = new ClientConfig();
+        pagedClientConfig.setMaxPageSizeInGetMetadata(pageSize);
+        MQClientAPIImpl pagedApi = new MQClientAPIImpl(new 
NettyClientConfig(), null, null, pagedClientConfig);
+        Field field = MQClientAPIImpl.class.getDeclaredField("remotingClient");
+        field.setAccessible(true);
+        field.set(pagedApi, remotingClient);
+
+        DataVersion dataVersion = new DataVersion();
+        SubscriptionGroupWrapper page1 = new SubscriptionGroupWrapper();
+        for (int i = 0; i < pageSize; i++) {
+            SubscriptionGroupConfig config = new SubscriptionGroupConfig();
+            config.setGroupName("group-" + i);
+            page1.getSubscriptionGroupTable().put("group-" + i, config);
+        }
+        page1.setDataVersion(dataVersion);
+        SubscriptionGroupWrapper page2 = new SubscriptionGroupWrapper();
+        SubscriptionGroupConfig lastConfig = new SubscriptionGroupConfig();
+        lastConfig.setGroupName("group-" + pageSize);
+        page2.getSubscriptionGroupTable().put("group-" + pageSize, lastConfig);
+        page2.setDataVersion(dataVersion);
+
+        GetAllSubscriptionGroupResponseHeader header1 = new 
GetAllSubscriptionGroupResponseHeader();
+        header1.setTotalGroupNum(total);
+        RemotingCommand resp1 = 
wireResponse(RemotingCommand.createResponseCommandWithHeader(ResponseCode.SUCCESS,
 header1));
+        resp1.setBody(RemotingSerializable.encode(page1));
+        GetAllSubscriptionGroupResponseHeader header2 = new 
GetAllSubscriptionGroupResponseHeader();
+        header2.setTotalGroupNum(total);
+        RemotingCommand resp2 = 
wireResponse(RemotingCommand.createResponseCommandWithHeader(ResponseCode.SUCCESS,
 header2));
+        resp2.setBody(RemotingSerializable.encode(page2));
+        when(remotingClient.invokeSync(anyString(), 
any(RemotingCommand.class), anyLong()))
+            .thenReturn(resp1, resp2);
+
+        SubscriptionGroupWrapper actual = 
pagedApi.getAllSubscriptionGroup(defaultBrokerAddr, defaultTimeout);
+
+        assertEquals(total, actual.getSubscriptionGroupTable().size());
+        assertNotNull(actual.getSubscriptionGroupTable().get("group-" + 
pageSize));
+    }
+
     @Test
     public void testUpdateNameServerConfig() throws RemotingException, 
InterruptedException, MQClientException, UnsupportedEncodingException {
         mockInvokeSync();
@@ -2138,6 +2224,16 @@ public class MQClientAPIImplTest {
         
when(response.getBody()).thenReturn(RemotingSerializable.encode(responseBody));
     }
 
+    /**
+     * Round-trips the response through encode/decode so response headers are 
carried as extFields,
+     * mirroring the wire format the client actually parses.
+     */
+    private static RemotingCommand wireResponse(RemotingCommand response) 
throws RemotingCommandException {
+        ByteBuffer frame = response.encode();
+        frame.position(4); // strip the total length the transport decoder 
consumes
+        return RemotingCommand.decode(frame.slice());
+    }
+
     private void mockInvokeSync() throws RemotingConnectException, 
RemotingSendRequestException, RemotingTimeoutException, InterruptedException {
         when(response.getCode()).thenReturn(ResponseCode.SUCCESS);
         when(response.getVersion()).thenReturn(1);

Reply via email to