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 5a6e349739 [ISSUE #11306] fix(tools): compare long counters without 
truncating to int (#11274)
5a6e349739 is described below

commit 5a6e349739bf2430d3d33250f8b1fcf92928405f
Author: zhang hongyuan <[email protected]>
AuthorDate: Thu Oct 8 14:46:23 2026 +0800

    [ISSUE #11306] fix(tools): compare long counters without truncating to int 
(#11274)
    
    GroupConsumeInfo.compareTo, TagCountBean.compareTo and
    QueryMsgByUniqueKeySubCommand.queryById subtracted two long values and cast
    the difference to int, so a difference beyond Integer.MAX_VALUE reversed the
    order and broke the comparator contract that TimSort checks. Use
    Long.compare / Comparator.comparingLong instead.
    
    Signed-off-by: zhang hongyuan <[email protected]>
---
 .../consumer/ConsumerProgressSubCommand.java       |  6 +-
 .../message/PrintMessageByQueueCommand.java        |  6 +-
 .../message/QueryMsgByUniqueKeySubCommand.java     | 12 ++-
 .../consumer/ConsumerProgressSubCommandTest.java   | 78 +++++++++++++++++++
 .../message/PrintMessageByQueueCommandTest.java    | 88 ++++++++++++++++++++++
 .../message/QueryMsgByUniqueKeySubCommandTest.java | 29 +++++++
 6 files changed, 216 insertions(+), 3 deletions(-)

diff --git 
a/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/ConsumerProgressSubCommand.java
 
b/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/ConsumerProgressSubCommand.java
index b638dcf61f..40fe52c6bc 100644
--- 
a/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/ConsumerProgressSubCommand.java
+++ 
b/tools/src/main/java/org/apache/rocketmq/tools/command/consumer/ConsumerProgressSubCommand.java
@@ -349,7 +349,11 @@ class GroupConsumeInfo implements 
Comparable<GroupConsumeInfo> {
             return o.count - this.count;
         }
 
-        return (int) (o.diffTotal - diffTotal);
+        // diffTotal is a message lag that can exceed Integer.MAX_VALUE, so 
compare the
+        // longs directly. Casting the subtraction to int wraps around for 
large gaps,
+        // which both reverses the reported ordering and breaks the Comparable 
contract
+        // that Collections.sort (TimSort) relies on.
+        return Long.compare(o.diffTotal, diffTotal);
     }
 
     public int getConsumeTps() {
diff --git 
a/tools/src/main/java/org/apache/rocketmq/tools/command/message/PrintMessageByQueueCommand.java
 
b/tools/src/main/java/org/apache/rocketmq/tools/command/message/PrintMessageByQueueCommand.java
index 0418e88a70..9933189ddd 100644
--- 
a/tools/src/main/java/org/apache/rocketmq/tools/command/message/PrintMessageByQueueCommand.java
+++ 
b/tools/src/main/java/org/apache/rocketmq/tools/command/message/PrintMessageByQueueCommand.java
@@ -250,7 +250,11 @@ public class PrintMessageByQueueCommand implements 
SubCommand {
 
         @Override
         public int compareTo(final TagCountBean o) {
-            return (int) (o.getCount().get() - this.count.get());
+            // Tag counts are longs that may exceed Integer.MAX_VALUE; the 
previous
+            // (int) (o.count - this.count) form truncated the difference, 
which
+            // inverted the order and violated the Comparable contract used by
+            // Collections.sort. Long.compare keeps the full 64-bit ordering.
+            return Long.compare(o.getCount().get(), this.count.get());
         }
     }
 }
diff --git 
a/tools/src/main/java/org/apache/rocketmq/tools/command/message/QueryMsgByUniqueKeySubCommand.java
 
b/tools/src/main/java/org/apache/rocketmq/tools/command/message/QueryMsgByUniqueKeySubCommand.java
index 8518a04e68..34b794999a 100644
--- 
a/tools/src/main/java/org/apache/rocketmq/tools/command/message/QueryMsgByUniqueKeySubCommand.java
+++ 
b/tools/src/main/java/org/apache/rocketmq/tools/command/message/QueryMsgByUniqueKeySubCommand.java
@@ -20,6 +20,7 @@ import java.io.DataOutputStream;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.util.Comparator;
 import java.util.List;
 import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.Option;
@@ -42,6 +43,15 @@ public class QueryMsgByUniqueKeySubCommand implements 
SubCommand {
 
     private DefaultMQAdminExt defaultMQAdminExt;
 
+    /**
+     * Orders messages by their store timestamp. The lambda it replaces 
subtracted two longs
+     * and cast the result to int, which wrapped around whenever the 
timestamps were more than
+     * Integer.MAX_VALUE milliseconds (about 24.8 days) apart, mis-sorting the 
output and
+     * breaking the comparator contract that TimSort enforces.
+     */
+    static final Comparator<MessageExt> STORE_TIMESTAMP_COMPARATOR =
+        Comparator.comparingLong(MessageExt::getStoreTimestamp);
+
     private DefaultMQAdminExt createMQAdminExt(RPCHook rpcHook) throws 
SubCommandException {
         if (this.defaultMQAdminExt != null) {
             return defaultMQAdminExt;
@@ -73,7 +83,7 @@ public class QueryMsgByUniqueKeySubCommand implements 
SubCommand {
         if (list == null || list.size() == 0) {
             return;
         }
-        list.sort((o1, o2) -> (int) (o1.getStoreTimestamp() - 
o2.getStoreTimestamp()));
+        list.sort(STORE_TIMESTAMP_COMPARATOR);
         for (int i = 0; i < (showAll ? list.size() : 1); i++) {
             showMessage(admin, list.get(i), i);
         }
diff --git 
a/tools/src/test/java/org/apache/rocketmq/tools/command/consumer/ConsumerProgressSubCommandTest.java
 
b/tools/src/test/java/org/apache/rocketmq/tools/command/consumer/ConsumerProgressSubCommandTest.java
index 15c3fa777a..9b8a064d5c 100644
--- 
a/tools/src/test/java/org/apache/rocketmq/tools/command/consumer/ConsumerProgressSubCommandTest.java
+++ 
b/tools/src/test/java/org/apache/rocketmq/tools/command/consumer/ConsumerProgressSubCommandTest.java
@@ -16,7 +16,11 @@
  */
 package org.apache.rocketmq.tools.command.consumer;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.DefaultParser;
 import org.apache.commons.cli.Options;
@@ -28,6 +32,7 @@ import org.apache.rocketmq.tools.command.SubCommandException;
 import org.apache.rocketmq.tools.command.server.NameServerMocker;
 import org.apache.rocketmq.tools.command.server.ServerResponseMocker;
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -81,4 +86,77 @@ public class ConsumerProgressSubCommandTest {
         // start broker
         return ServerResponseMocker.startServer(consumeStats.encode());
     }
+
+    @Test
+    public void testCompareToIsAntisymmetricAcrossIntegerMaxValue() {
+        GroupConsumeInfo zeroLag = buildGroupConsumeInfo("zero-lag", 0L);
+        GroupConsumeInfo overIntLag = buildGroupConsumeInfo("over-int-lag", 
(long) Integer.MAX_VALUE + 1L);
+        GroupConsumeInfo maxLag = buildGroupConsumeInfo("max-lag", 
Long.MAX_VALUE);
+
+        // A subtraction-based comparator wraps for gaps beyond 
Integer.MAX_VALUE and would
+        // then report both a < b and b < a; assert the antisymmetric contract 
explicitly.
+        Assert.assertTrue(zeroLag.compareTo(overIntLag) > 0);
+        Assert.assertTrue(overIntLag.compareTo(zeroLag) < 0);
+        Assert.assertTrue(overIntLag.compareTo(maxLag) > 0);
+        Assert.assertTrue(maxLag.compareTo(overIntLag) < 0);
+        Assert.assertEquals(0, 
overIntLag.compareTo(buildGroupConsumeInfo("over-int-lag-copy", (long) 
Integer.MAX_VALUE + 1L)));
+        Assert.assertEquals(0, 
maxLag.compareTo(buildGroupConsumeInfo("max-lag-copy", Long.MAX_VALUE)));
+    }
+
+    @Test
+    public void testCollectionsSortOrdersGroupsByLagDescending() {
+        GroupConsumeInfo zeroLag = buildGroupConsumeInfo("zero-lag", 0L);
+        GroupConsumeInfo overIntLag = buildGroupConsumeInfo("over-int-lag", 
(long) Integer.MAX_VALUE + 1L);
+        GroupConsumeInfo maxLag = buildGroupConsumeInfo("max-lag", 
Long.MAX_VALUE);
+
+        List<GroupConsumeInfo> groups = new ArrayList<>();
+        groups.add(zeroLag);
+        groups.add(maxLag);
+        groups.add(overIntLag);
+        Collections.sort(groups);
+
+        Assert.assertEquals(Arrays.asList(maxLag, overIntLag, zeroLag), 
groups);
+    }
+
+    @Test
+    public void testSortWithLagValuesSpanningIntAndLongRanges() {
+        List<GroupConsumeInfo> groups = new ArrayList<>();
+        groups.add(buildGroupConsumeInfo("lag-zero", 0L));
+        groups.add(buildGroupConsumeInfo("lag-small", 1024L));
+        groups.add(buildGroupConsumeInfo("lag-over-int", (long) 
Integer.MAX_VALUE + 1L));
+        groups.add(buildGroupConsumeInfo("lag-max", Long.MAX_VALUE));
+
+        Collections.sort(groups);
+
+        // Every adjacent pair must be ordered descending, which only holds 
when the
+        // comparison never wraps around for gaps larger than 
Integer.MAX_VALUE.
+        for (int i = 0; i < groups.size() - 1; i++) {
+            Assert.assertTrue(groups.get(i).getDiffTotal() > groups.get(i + 
1).getDiffTotal());
+            Assert.assertTrue(groups.get(i).compareTo(groups.get(i + 1)) < 0);
+        }
+        Assert.assertEquals("lag-max", groups.get(0).getGroup());
+        Assert.assertEquals("lag-zero", groups.get(groups.size() - 
1).getGroup());
+    }
+
+    @Test
+    public void testCompareToPrefersMoreConsumersBeforeLag() {
+        GroupConsumeInfo singleConsumer = 
buildGroupConsumeInfo("single-consumer", Long.MAX_VALUE);
+        GroupConsumeInfo twoConsumers = buildGroupConsumeInfo("two-consumers", 
0L);
+        twoConsumers.setCount(2);
+
+        // The count comparison short-circuits the diffTotal comparison: 
groups with more
+        // live connections are always ordered first, whatever their lag is.
+        Assert.assertTrue(singleConsumer.compareTo(twoConsumers) > 0);
+        Assert.assertTrue(twoConsumers.compareTo(singleConsumer) < 0);
+    }
+
+    private static GroupConsumeInfo buildGroupConsumeInfo(String group, long 
diffTotal) {
+        GroupConsumeInfo info = new GroupConsumeInfo();
+        info.setGroup(group);
+        // Keep the connection count identical so compareTo falls through to 
the diffTotal
+        // comparison that is under test.
+        info.setCount(1);
+        info.setDiffTotal(diffTotal);
+        return info;
+    }
 }
diff --git 
a/tools/src/test/java/org/apache/rocketmq/tools/command/message/PrintMessageByQueueCommandTest.java
 
b/tools/src/test/java/org/apache/rocketmq/tools/command/message/PrintMessageByQueueCommandTest.java
new file mode 100644
index 0000000000..ac5f8a29a0
--- /dev/null
+++ 
b/tools/src/test/java/org/apache/rocketmq/tools/command/message/PrintMessageByQueueCommandTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.tools.command.message;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class PrintMessageByQueueCommandTest {
+
+    @Test
+    public void testCompareToIsAntisymmetricBeyondIntegerMaxValue() {
+        PrintMessageByQueueCommand.TagCountBean zero = 
newTagCountBean("tag-zero", 0L);
+        PrintMessageByQueueCommand.TagCountBean overInt = 
newTagCountBean("tag-over-int", (long) Integer.MAX_VALUE + 1L);
+
+        // (int) (o.count - this.count) wraps for this pair and returns a 
negative value in
+        // both directions, so assert the sign of the comparison is properly 
inverted.
+        Assert.assertTrue(zero.compareTo(overInt) > 0);
+        Assert.assertTrue(overInt.compareTo(zero) < 0);
+        Assert.assertEquals(Integer.signum(overInt.compareTo(zero)), 
-Integer.signum(zero.compareTo(overInt)));
+    }
+
+    @Test
+    public void testCollectionsSortOrdersTagCountsDescending() {
+        PrintMessageByQueueCommand.TagCountBean zero = 
newTagCountBean("tag-zero", 0L);
+        PrintMessageByQueueCommand.TagCountBean overInt = 
newTagCountBean("tag-over-int", (long) Integer.MAX_VALUE + 1L);
+        PrintMessageByQueueCommand.TagCountBean max = 
newTagCountBean("tag-max", Long.MAX_VALUE);
+
+        List<PrintMessageByQueueCommand.TagCountBean> beans = new 
ArrayList<>();
+        beans.add(zero);
+        beans.add(max);
+        beans.add(overInt);
+        Collections.sort(beans);
+
+        Assert.assertEquals(Arrays.asList("tag-max", "tag-over-int", 
"tag-zero"), tagsOf(beans));
+    }
+
+    @Test
+    public void testEqualCountsCompareEqual() {
+        PrintMessageByQueueCommand.TagCountBean first = 
newTagCountBean("tag-first", (long) Integer.MAX_VALUE + 1L);
+        PrintMessageByQueueCommand.TagCountBean second = 
newTagCountBean("tag-second", (long) Integer.MAX_VALUE + 1L);
+
+        // Equal counts must produce an equal comparison in both directions, 
otherwise
+        // TimSort treats the two entries as an inconsistent pair.
+        Assert.assertEquals(0, first.compareTo(second));
+        Assert.assertEquals(0, second.compareTo(first));
+    }
+
+    @Test
+    public void testCompareToHandlesLongMaxValueCounts() {
+        PrintMessageByQueueCommand.TagCountBean overInt = 
newTagCountBean("tag-over-int", (long) Integer.MAX_VALUE + 1L);
+        PrintMessageByQueueCommand.TagCountBean max = 
newTagCountBean("tag-max", Long.MAX_VALUE);
+
+        // The largest possible gap between two long counters must still order 
correctly.
+        Assert.assertTrue(overInt.compareTo(max) > 0);
+        Assert.assertTrue(max.compareTo(overInt) < 0);
+    }
+
+    private static PrintMessageByQueueCommand.TagCountBean 
newTagCountBean(String tag, long count) {
+        return new PrintMessageByQueueCommand.TagCountBean(tag, new 
AtomicLong(count));
+    }
+
+    private static List<String> 
tagsOf(List<PrintMessageByQueueCommand.TagCountBean> beans) {
+        List<String> tags = new ArrayList<>(beans.size());
+        for (PrintMessageByQueueCommand.TagCountBean bean : beans) {
+            tags.add(bean.getTag());
+        }
+        return tags;
+    }
+}
diff --git 
a/tools/src/test/java/org/apache/rocketmq/tools/command/message/QueryMsgByUniqueKeySubCommandTest.java
 
b/tools/src/test/java/org/apache/rocketmq/tools/command/message/QueryMsgByUniqueKeySubCommandTest.java
index b24bd22db8..1a535dec42 100644
--- 
a/tools/src/test/java/org/apache/rocketmq/tools/command/message/QueryMsgByUniqueKeySubCommandTest.java
+++ 
b/tools/src/test/java/org/apache/rocketmq/tools/command/message/QueryMsgByUniqueKeySubCommandTest.java
@@ -19,6 +19,7 @@ package org.apache.rocketmq.tools.command.message;
 import java.lang.reflect.Field;
 import java.net.InetSocketAddress;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -58,6 +59,7 @@ import org.apache.rocketmq.tools.admin.DefaultMQAdminExt;
 import org.apache.rocketmq.tools.admin.DefaultMQAdminExtImpl;
 import org.apache.rocketmq.tools.command.SubCommandException;
 import org.assertj.core.util.Lists;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -253,4 +255,31 @@ public class QueryMsgByUniqueKeySubCommandTest {
         cmd.execute(commandLine, options, null);
 
     }
+
+    @Test
+    public void 
testStoreTimestampComparatorKeepsOlderMessageFirstAcrossIntRange() {
+        MessageExt newer = new MessageExt();
+        newer.setMsgId("newer-message");
+        newer.setStoreTimestamp(System.currentTimeMillis());
+
+        MessageExt older = new MessageExt();
+        older.setMsgId("older-message");
+        // More than Integer.MAX_VALUE milliseconds (roughly 24.8 days) 
separate the two
+        // messages, so the old (int) (o1 - o2) lambda wrapped and reversed 
their order.
+        long gap = (long) Integer.MAX_VALUE + 1L;
+        older.setStoreTimestamp(newer.getStoreTimestamp() - gap);
+
+        // The comparator itself must report the older message as smaller in 
both directions.
+        
Assert.assertTrue(QueryMsgByUniqueKeySubCommand.STORE_TIMESTAMP_COMPARATOR.compare(older,
 newer) < 0);
+        
Assert.assertTrue(QueryMsgByUniqueKeySubCommand.STORE_TIMESTAMP_COMPARATOR.compare(newer,
 older) > 0);
+
+        List<MessageExt> messages = new ArrayList<>();
+        messages.add(newer);
+        messages.add(older);
+        Collections.sort(messages, 
QueryMsgByUniqueKeySubCommand.STORE_TIMESTAMP_COMPARATOR);
+
+        Assert.assertSame(older, messages.get(0));
+        Assert.assertSame(newer, messages.get(1));
+        Assert.assertEquals(0, 
QueryMsgByUniqueKeySubCommand.STORE_TIMESTAMP_COMPARATOR.compare(older, older));
+    }
 }

Reply via email to