Repository: apache/rocketmq (Java)
Task type: Feature

Issue:
[Enhancement] Pop retry topic v2
### Before Creating the Enhancement Request

- [X] I have confirmed that this should be classified as an enhancement rather than a bug/feature.


### Summary

目前在pop机制中使用的retry topic的格式是形如 "%RETRY%group_topic"，其中分隔符"_"是一个合法字符，可以被使用在topic或者group中，因此会导致两个问题。

1. pop retry topic重复，比如由group_a，topic组成的retry topic，和由group，a_topic组成的retry topic均为%RETRY%group_a_topic
2. 由于采用了合法字符"_"作为分隔符，因此没办法根据retry topic反向得出group和topic，在消息通知和需要拆分group的场景都会存在问题

因此本方案提出了pop retry topic v2，以"+"为分割符，即形如"%RETRY%group+topic",其中"+"为topic和group无法使用的字符，因此解决了上述两个问题，同时"+"也隐式的表达了订阅的含义。


Currently, in the pop mechanism, the format used for the retry topic is as the form of "%RETRY%group_topic". The underscore character "_" serves as a valid separator, which can be used within both the topic and group. Consequently, this situation leads to two issues:

1. Repetition of pop retry topics: For instance, a retry topic composed of group_a and topic, and another retry topic composed of group and a_topic, both result in "%RETRY%group_a_topic".
2. Due to the utilization of the underscore character "_" as a separator, it becomes impossible to deduce the group and topic from the retry topic. This presents challenges in scenarios involving message notifications and the need to split the group.

Therefore, this proposal suggests the implementation of pop retry topic v2, employing the "+" as the separator. The new format would appear as "%RETRY%group+topic". Since "+" is not a permissible character in either topics or groups, it effectively resolves the aforementioned issues. Furthermore, the use of "+" implicitly expresses the subscription meaning.

### Motivation

Fix the issue of original pop retry topic.

### Describe the Solution You'd Like

Retry topic v2 as the format of "%RETRY%group+topic" where "+" is the separator.

### Describe Alternatives You've Considered

No

### Additional Context

_No response_


Discussion:

Solution patch:
diff --git a/common/src/main/java/org/apache/rocketmq/common/KeyBuilder.java b/common/src/main/java/org/apache/rocketmq/common/KeyBuilder.java
--- a/common/src/main/java/org/apache/rocketmq/common/KeyBuilder.java
+++ b/common/src/main/java/org/apache/rocketmq/common/KeyBuilder.java
@@ -18,8 +18,9 @@
 
 public class KeyBuilder {
     public static final int POP_ORDER_REVIVE_QUEUE = 999;
-    private static final String POP_RETRY_SEPARATOR_V1 = "_";
-    private static final String POP_RETRY_SEPARATOR_V2 = ":";
+    private static final char POP_RETRY_SEPARATOR_V1 = '_';
+    private static final char POP_RETRY_SEPARATOR_V2 = '+';
+    private static final String POP_RETRY_REGEX_SEPARATOR_V2 = "\\+";
 
     public static String buildPopRetryTopic(String topic, String cid) {
         return MixAll.RETRY_GROUP_TOPIC_PREFIX + cid + POP_RETRY_SEPARATOR_V2 + topic;
@@ -42,7 +43,7 @@ public static String parseNormalTopic(String topic, String cid) {
 
     public static String parseNormalTopic(String retryTopic) {
         if (isPopRetryTopicV2(retryTopic)) {
-            String[] result = retryTopic.split(POP_RETRY_SEPARATOR_V2);
+            String[] result = retryTopic.split(POP_RETRY_REGEX_SEPARATOR_V2);
             if (result.length == 2) {
                 return result[1];
             }
@@ -52,7 +53,7 @@ public static String parseNormalTopic(String retryTopic) {
 
     public static String parseGroup(String retryTopic) {
         if (isPopRetryTopicV2(retryTopic)) {
-            String[] result = retryTopic.split(POP_RETRY_SEPARATOR_V2);
+            String[] result = retryTopic.split(POP_RETRY_REGEX_SEPARATOR_V2);
             if (result.length == 2) {
                 return result[0].substring(MixAll.RETRY_GROUP_TOPIC_PREFIX.length());
             }
@@ -65,6 +66,6 @@ public static String buildPollingKey(String topic, String cid, int queueId) {
     }
 
     public static boolean isPopRetryTopicV2(String retryTopic) {
-        return retryTopic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX) && retryTopic.contains(POP_RETRY_SEPARATOR_V2);
+        return retryTopic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX) && retryTopic.contains(String.valueOf(POP_RETRY_SEPARATOR_V2));
     }
 }


Test patch:
diff --git a/common/src/test/java/org/apache/rocketmq/common/KeyBuilderTest.java b/common/src/test/java/org/apache/rocketmq/common/KeyBuilderTest.java
--- a/common/src/test/java/org/apache/rocketmq/common/KeyBuilderTest.java
+++ b/common/src/test/java/org/apache/rocketmq/common/KeyBuilderTest.java
@@ -26,37 +26,35 @@ public class KeyBuilderTest {
     String group = "test-group";
 
     @Test
-    public void buildPopRetryTopic() {
-        assertThat(KeyBuilder.buildPopRetryTopic(topic, group)).isEqualTo(MixAll.RETRY_GROUP_TOPIC_PREFIX + group + ":" + topic);
+    public void testBuildPopRetryTopic() {
+        assertThat(KeyBuilder.buildPopRetryTopic(topic, group)).isEqualTo(MixAll.RETRY_GROUP_TOPIC_PREFIX + group + "+" + topic);
     }
 
     @Test
-    public void buildPopRetryTopicV1() {
+    public void testBuildPopRetryTopicV1() {
         assertThat(KeyBuilder.buildPopRetryTopicV1(topic, group)).isEqualTo(MixAll.RETRY_GROUP_TOPIC_PREFIX + group + "_" + topic);
     }
 
     @Test
-    public void parseNormalTopic() {
+    public void testParseNormalTopic() {
         String popRetryTopic = KeyBuilder.buildPopRetryTopic(topic, group);
         assertThat(KeyBuilder.parseNormalTopic(popRetryTopic, group)).isEqualTo(topic);
+
         String popRetryTopicV1 = KeyBuilder.buildPopRetryTopicV1(topic, group);
         assertThat(KeyBuilder.parseNormalTopic(popRetryTopicV1, group)).isEqualTo(topic);
-    }
 
-    @Test
-    public void testParseNormalTopic() {
-        String popRetryTopic = KeyBuilder.buildPopRetryTopic(topic, group);
+        popRetryTopic = KeyBuilder.buildPopRetryTopic(topic, group);
         assertThat(KeyBuilder.parseNormalTopic(popRetryTopic)).isEqualTo(topic);
     }
 
     @Test
-    public void parseGroup() {
+    public void testParseGroup() {
         String popRetryTopic = KeyBuilder.buildPopRetryTopic(topic, group);
         assertThat(KeyBuilder.parseGroup(popRetryTopic)).isEqualTo(group);
     }
 
     @Test
-    public void isPopRetryTopicV2() {
+    public void testIsPopRetryTopicV2() {
         String popRetryTopic = KeyBuilder.buildPopRetryTopic(topic, group);
         assertThat(KeyBuilder.isPopRetryTopicV2(popRetryTopic)).isEqualTo(true);
         String popRetryTopicV1 = KeyBuilder.buildPopRetryTopicV1(topic, group);