This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new f440f7e78 fix(provider): avoid topic consumer pagination overflow
(#2198)
f440f7e78 is described below
commit f440f7e78a2b2fd5e8446a5613cbdf9e0fdf69ac
Author: btlqql <[email protected]>
AuthorDate: Tue Aug 18 18:55:14 2026 +0800
fix(provider): avoid topic consumer pagination overflow (#2198)
Calculate pagination offsets with long arithmetic so very large validated
page numbers return an empty page instead of wrapping to a negative index.
Signed-off-by: btlqql <[email protected]>
---
.../rocketmq/studio/common/util/Pagination.java | 42 ++++++++++++++++
.../rocketmq/studio/provider/InstanceProvider.java | 6 ++-
.../studio/provider/apache/MetadataProvider.java | 6 ++-
.../provider/apache/RocketMQMetadataProvider.java | 6 ++-
.../studio/common/util/PaginationTest.java | 57 ++++++++++++++++++++++
.../studio/provider/InstanceProviderTest.java | 47 ++++++++++++++++++
.../apache/RocketMQMetadataProviderTest.java | 35 +++++++++++++
7 files changed, 193 insertions(+), 6 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/common/util/Pagination.java
b/server/src/main/java/org/apache/rocketmq/studio/common/util/Pagination.java
new file mode 100644
index 000000000..356fcbd64
--- /dev/null
+++
b/server/src/main/java/org/apache/rocketmq/studio/common/util/Pagination.java
@@ -0,0 +1,42 @@
+/*
+ * 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.studio.common.util;
+
+/** Pagination arithmetic shared by in-memory page slicing across providers. */
+public final class Pagination {
+
+ private Pagination() {
+ }
+
+ /**
+ * Computes the zero-based offset of the requested page with long
arithmetic, so very
+ * large page numbers never wrap to a negative index. Products that would
overflow a
+ * {@code long} are capped at {@link Long#MAX_VALUE}; callers treat any
offset at or
+ * beyond their total element count as an empty page.
+ */
+ public static long pageOffset(long page, int pageSize) {
+ long safePage = Math.max(page, 1L);
+ long size = Math.max(pageSize, 0);
+ if (size == 0) {
+ return 0L;
+ }
+ if (safePage - 1 > Long.MAX_VALUE / size) {
+ return Long.MAX_VALUE;
+ }
+ return (safePage - 1) * size;
+ }
+}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/provider/InstanceProvider.java
b/server/src/main/java/org/apache/rocketmq/studio/provider/InstanceProvider.java
index 9cdecb692..43e64627d 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/provider/InstanceProvider.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/provider/InstanceProvider.java
@@ -17,6 +17,7 @@
package org.apache.rocketmq.studio.provider;
import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
+import org.apache.rocketmq.studio.common.util.Pagination;
import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
import org.apache.rocketmq.studio.instance.group.QueueProgressVO;
import org.apache.rocketmq.studio.instance.group.SubscriptionEntryVO;
@@ -54,8 +55,9 @@ public interface InstanceProvider {
default TopicConsumerPageVO getTopicConsumersPage(String instanceId,
String topicName, int page, int pageSize) {
List<TopicConsumerVO> consumers = getTopicConsumers(instanceId,
topicName);
int total = consumers.size();
- int from = Math.min((page - 1) * pageSize, total);
- int to = Math.min(from + pageSize, total);
+ long offset = Pagination.pageOffset(page, pageSize);
+ int from = (int) Math.min(offset, total);
+ int to = from + (int) Math.min(pageSize, total - from);
return TopicConsumerPageVO.builder()
.items(consumers.subList(from, to))
.total(total)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/MetadataProvider.java
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/MetadataProvider.java
index f66b7fb17..b565055dd 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/MetadataProvider.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/MetadataProvider.java
@@ -16,6 +16,7 @@
*/
package org.apache.rocketmq.studio.provider.apache;
+import org.apache.rocketmq.studio.common.util.Pagination;
import org.apache.rocketmq.studio.instance.topic.TopicConsumerVO;
import org.apache.rocketmq.studio.instance.topic.TopicConsumerPageVO;
import org.apache.rocketmq.studio.instance.topic.BrokerRouteVO;
@@ -40,8 +41,9 @@ public interface MetadataProvider {
default TopicConsumerPageVO getTopicConsumersPage(String instanceId,
String name, int page, int pageSize) {
List<TopicConsumerVO> consumers = getTopicConsumers(instanceId, name);
int total = consumers.size();
- int from = Math.min((page - 1) * pageSize, total);
- int to = Math.min(from + pageSize, total);
+ long offset = Pagination.pageOffset(page, pageSize);
+ int from = (int) Math.min(offset, total);
+ int to = from + (int) Math.min(pageSize, total - from);
return TopicConsumerPageVO.builder()
.items(consumers.subList(from, to))
.total(total)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProvider.java
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProvider.java
index a14711e61..4882beeed 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProvider.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProvider.java
@@ -37,6 +37,7 @@ import org.apache.rocketmq.tools.admin.MQAdminExt;
import org.apache.rocketmq.studio.common.domain.enums.ConsumeType;
import org.apache.rocketmq.studio.common.domain.enums.SubscriptionMode;
import org.apache.rocketmq.studio.common.domain.enums.TopicPerm;
+import org.apache.rocketmq.studio.common.util.Pagination;
import org.apache.rocketmq.studio.common.util.SystemGroupFilter;
import org.apache.rocketmq.studio.common.util.SystemTopicFilter;
import org.apache.rocketmq.studio.common.domain.enums.TopicType;
@@ -312,8 +313,9 @@ public class RocketMQMetadataProvider implements
MetadataProvider {
List<String> sortedGroups = new ArrayList<>(subscribingGroups);
sortedGroups.sort(String::compareToIgnoreCase);
int total = sortedGroups.size();
- int from = Math.min((page - 1) * pageSize, total);
- int to = Math.min(from + pageSize, total);
+ long offset = Pagination.pageOffset(page, pageSize);
+ int from = (int) Math.min(offset, total);
+ int to = from + (int) Math.min(pageSize, total - from);
List<TopicConsumerVO> consumers = new ArrayList<>();
for (String group : sortedGroups.subList(from, to)) {
try {
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/common/util/PaginationTest.java
b/server/src/test/java/org/apache/rocketmq/studio/common/util/PaginationTest.java
new file mode 100644
index 000000000..f7cc5ecba
--- /dev/null
+++
b/server/src/test/java/org/apache/rocketmq/studio/common/util/PaginationTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.studio.common.util;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class PaginationTest {
+
+ @Test
+ void pageOffsetShouldComputeZeroBasedOffset() {
+ assertThat(Pagination.pageOffset(1, 20)).isEqualTo(0L);
+ assertThat(Pagination.pageOffset(3, 20)).isEqualTo(40L);
+ }
+
+ @Test
+ void pageOffsetShouldUseLongArithmeticForLargePages() {
+ assertThat(Pagination.pageOffset(Integer.MAX_VALUE, 100))
+ .isEqualTo((Integer.MAX_VALUE - 1L) * 100);
+ assertThat(Pagination.pageOffset(Integer.MAX_VALUE, Integer.MAX_VALUE))
+ .isEqualTo((Integer.MAX_VALUE - 1L) * Integer.MAX_VALUE);
+ }
+
+ @Test
+ void pageOffsetShouldCapWhenLongProductWouldOverflow() {
+ assertThat(Pagination.pageOffset(Long.MAX_VALUE,
Integer.MAX_VALUE)).isEqualTo(Long.MAX_VALUE);
+ assertThat(Pagination.pageOffset(Long.MAX_VALUE / 2 + 2,
2)).isEqualTo(Long.MAX_VALUE);
+ }
+
+ @Test
+ void pageOffsetShouldNotCapProductThatExactlyFitsInLong() {
+ assertThat(Pagination.pageOffset(Long.MAX_VALUE,
1)).isEqualTo(Long.MAX_VALUE - 1);
+ }
+
+ @Test
+ void pageOffsetShouldClampNonPositiveInputsToFirstPage() {
+ assertThat(Pagination.pageOffset(0, 20)).isEqualTo(0L);
+ assertThat(Pagination.pageOffset(-5, 20)).isEqualTo(0L);
+ assertThat(Pagination.pageOffset(5, 0)).isEqualTo(0L);
+ assertThat(Pagination.pageOffset(5, -1)).isEqualTo(0L);
+ }
+}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/provider/InstanceProviderTest.java
b/server/src/test/java/org/apache/rocketmq/studio/provider/InstanceProviderTest.java
new file mode 100644
index 000000000..2c0776eaa
--- /dev/null
+++
b/server/src/test/java/org/apache/rocketmq/studio/provider/InstanceProviderTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.studio.provider;
+
+import java.util.Arrays;
+import org.apache.rocketmq.studio.instance.topic.TopicConsumerPageVO;
+import org.apache.rocketmq.studio.instance.topic.TopicConsumerVO;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class InstanceProviderTest {
+
+ @Test
+ public void getTopicConsumersPageShouldHandleLargePageNumberTest() {
+ InstanceProvider provider = mock(InstanceProvider.class);
+ when(provider.getTopicConsumers("instance-a",
"orders")).thenReturn(Arrays.asList(
+ TopicConsumerVO.builder().group("group-a").build(),
+ TopicConsumerVO.builder().group("group-b").build()));
+ when(provider.getTopicConsumersPage("instance-a", "orders",
Integer.MAX_VALUE, 100))
+ .thenCallRealMethod();
+
+ TopicConsumerPageVO result = provider.getTopicConsumersPage(
+ "instance-a", "orders", Integer.MAX_VALUE, 100);
+
+ assertThat(result.getItems()).isEmpty();
+ assertThat(result.getTotal()).isEqualTo(2);
+ assertThat(result.getPage()).isEqualTo(Integer.MAX_VALUE);
+ assertThat(result.getPageSize()).isEqualTo(100);
+ }
+}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProviderTest.java
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProviderTest.java
index f8459f52d..9baeeba4e 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProviderTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQMetadataProviderTest.java
@@ -196,6 +196,41 @@ class RocketMQMetadataProviderTest {
verify(admin, never()).examineConsumeStats("group-b", "TopicA");
}
+ @Test
+ void getTopicConsumersPageShouldReturnEmptyPageForLargePageNumberTest()
throws Exception {
+ DefaultMQAdminExt admin = mock(DefaultMQAdminExt.class);
+ GroupList groups = new GroupList();
+ groups.setGroupList(new HashSet<>(List.of("group-a", "group-b")));
+ when(admin.queryTopicConsumeByWho("TopicA")).thenReturn(groups);
+
+ TopicConsumerPageVO result = newLiveProvider(admin)
+ .getTopicConsumersPage(null, "TopicA", Integer.MAX_VALUE, 100);
+
+ assertThat(result.getItems()).isEmpty();
+ assertThat(result.getTotal()).isEqualTo(2);
+ assertThat(result.getPage()).isEqualTo(Integer.MAX_VALUE);
+ assertThat(result.getPageSize()).isEqualTo(100);
+ verify(admin, never()).examineConsumeStats(anyString(), eq("TopicA"));
+ }
+
+ @Test
+ void
metadataProviderDefaultPageShouldReturnEmptyPageForLargePageNumberTest() {
+ MetadataProvider provider = mock(MetadataProvider.class);
+ when(provider.getTopicConsumers("instance-a",
"orders")).thenReturn(List.of(
+ TopicConsumerVO.builder().group("group-a").build(),
+ TopicConsumerVO.builder().group("group-b").build()));
+ when(provider.getTopicConsumersPage("instance-a", "orders",
Integer.MAX_VALUE, 100))
+ .thenCallRealMethod();
+
+ TopicConsumerPageVO result = provider.getTopicConsumersPage(
+ "instance-a", "orders", Integer.MAX_VALUE, 100);
+
+ assertThat(result.getItems()).isEmpty();
+ assertThat(result.getTotal()).isEqualTo(2);
+ assertThat(result.getPage()).isEqualTo(Integer.MAX_VALUE);
+ assertThat(result.getPageSize()).isEqualTo(100);
+ }
+
@Test
void groupRuntimeDiagnosticsShouldUseSelectedInstanceRuntimeClient() {
List<QueueProgressVO> progress =
List.of(QueueProgressVO.builder().broker("broker-a").build());