RockteMQ-AI commented on code in PR #145:
URL: https://github.com/apache/rocketmq-connect/pull/145#discussion_r3839568052


##########
connectors/rocketmq-replicator/src/main/java/org/apache/rocketmq/replicator/MetaSourceTask.java:
##########
@@ -120,28 +128,38 @@ public void resume() {
         List<ConnectRecord> res = new ArrayList<>();
         for (String group : groups) {
             ConsumeStats stats;
+            String brokerAddresMaster="";
+            String brokerName="";
             try {
                 stats = this.srcMQAdminExt.examineConsumeStats(group);
+                ClusterInfo clusterInfo = 
this.tarMQAdminExt.examineBrokerClusterInfo();
+                HashMap<String, Set<String>> clusterAddrTable = 
clusterInfo.getClusterAddrTable();
+                HashMap<String, BrokerData> brokerAddrTable = 
clusterInfo.getBrokerAddrTable();
+                Set<String> clusterNameSet = 
clusterAddrTable.get(this.config.getTargetCluster());

Review Comment:
   NPE risk: `clusterAddrTable.get(this.config.getTargetCluster())` can return 
null if the target cluster name is misconfigured or not yet registered in the 
target cluster topology. The subsequent `clusterNameSet.iterator()` would throw 
NullPointerException with no useful error message. Add a null check and log a 
meaningful error identifying the missing cluster.



##########
rocketmq-connect-runtime/src/main/java/org/apache/rocketmq/connect/runtime/connectorwrapper/WorkerSourceTask.java:
##########
@@ -307,7 +312,53 @@ private void sendRecord() throws InterruptedException, 
RemotingException, MQClie
                         log.error("Send record, message size is greater than 
{} bytes, sourceDataEntry: {}", RuntimeConfigDefine.MAX_MESSAGE_SIZE, 
JSON.toJSONString(sourceDataEntry));
                         continue;
                     }
+                    String targetTopic = sourceDataEntry.getExtension("topic");
+                    if (targetTopic != null){
+                        sourceMessage.setTopic(targetTopic);
+                    }
                     sourceMessage.setBody(messageBody);
+                    int queueId = 
sourceDataEntry.getExtensions().getInt("queueId");

Review Comment:
   NPE risk: `sourceDataEntry.getExtensions().getInt("queueId")` assumes 
getExtensions() is non-null and contains the "queueId" key. For any record not 
produced by RmqSourceTask (which is the only place queueId is set), this throws 
NPE or NoSuchElementException. Should null-check getExtensions() and provide a 
safe default or skip the MessageQueue-specific send path.



##########
connectors/rocketmq-replicator/src/main/java/org/apache/rocketmq/replicator/MetaSourceTask.java:
##########
@@ -120,28 +128,38 @@ public void resume() {
         List<ConnectRecord> res = new ArrayList<>();
         for (String group : groups) {
             ConsumeStats stats;
+            String brokerAddresMaster="";
+            String brokerName="";
             try {
                 stats = this.srcMQAdminExt.examineConsumeStats(group);
+                ClusterInfo clusterInfo = 
this.tarMQAdminExt.examineBrokerClusterInfo();
+                HashMap<String, Set<String>> clusterAddrTable = 
clusterInfo.getClusterAddrTable();
+                HashMap<String, BrokerData> brokerAddrTable = 
clusterInfo.getBrokerAddrTable();
+                Set<String> clusterNameSet = 
clusterAddrTable.get(this.config.getTargetCluster());
+                Iterator<String> it = clusterNameSet.iterator();
+                while (it.hasNext()){
+                   String clusterName = it.next();
+                   BrokerData brokerData = brokerAddrTable.get(clusterName);

Review Comment:
   NPE risk: `brokerAddrTable.get(clusterName)` may return null if a broker 
listed in the cluster table is not yet in the broker address table (race during 
broker add/remove). The subsequent `brokerData.getBrokerAddrs()` would NPE. 
Additionally, `brokerAddrs.get(new Long(0))` may return null if no master 
(brokerId=0) is available, causing `updateConsumeOffset(null, ...)` to silently 
fail inside the catch block. Add null checks for brokerData, brokerAddrs, and 
brokerAddresMaster.



##########
connectors/rocketmq-replicator/src/main/java/org/apache/rocketmq/replicator/MetaSourceTask.java:
##########
@@ -120,28 +128,38 @@ public void resume() {
         List<ConnectRecord> res = new ArrayList<>();
         for (String group : groups) {
             ConsumeStats stats;
+            String brokerAddresMaster="";
+            String brokerName="";
             try {
                 stats = this.srcMQAdminExt.examineConsumeStats(group);
+                ClusterInfo clusterInfo = 
this.tarMQAdminExt.examineBrokerClusterInfo();
+                HashMap<String, Set<String>> clusterAddrTable = 
clusterInfo.getClusterAddrTable();
+                HashMap<String, BrokerData> brokerAddrTable = 
clusterInfo.getBrokerAddrTable();
+                Set<String> clusterNameSet = 
clusterAddrTable.get(this.config.getTargetCluster());
+                Iterator<String> it = clusterNameSet.iterator();
+                while (it.hasNext()){

Review Comment:
   Performance: The while-loop iterates all brokers in the target cluster, and 
for each broker iterates ALL message queues from the consume stats, only 
updating the offset when `brokerName.equals(mq.getBrokerName())`. This is 
O(brokers x queues). Build a Map<brokerName, brokerAddress> first, then iterate 
the message queues once and look up the matching broker address.



##########
connectors/rocketmq-replicator/src/main/java/org/apache/rocketmq/replicator/MetaSourceTask.java:
##########
@@ -120,28 +128,38 @@ public void resume() {
         List<ConnectRecord> res = new ArrayList<>();
         for (String group : groups) {
             ConsumeStats stats;
+            String brokerAddresMaster="";
+            String brokerName="";
             try {
                 stats = this.srcMQAdminExt.examineConsumeStats(group);
+                ClusterInfo clusterInfo = 
this.tarMQAdminExt.examineBrokerClusterInfo();

Review Comment:
   Performance: `this.tarMQAdminExt.examineBrokerClusterInfo()` is called 
inside the per-group loop. This is an expensive RPC that returns the same 
target cluster topology for every consumer group. Hoist this call outside the 
for-loop and reuse the result across all groups.



##########
connectors/rocketmq-replicator/src/main/java/org/apache/rocketmq/replicator/RmqSourceTask.java:
##########
@@ -178,6 +179,11 @@ private List<ConnectRecord> pollCommonMessage() {
                                 final Map<String, String> properties = 
msg.getProperties();
                                 final Set<String> keys = properties.keySet();
                                 keys.forEach(key -> 
connectRecord.addExtension(key, properties.get(key)));
+                                
connectRecord.addExtension("topic",taskTopicConfig.getTargetTopic());
+                                
connectRecord.addExtension("brokerName",msg.getBrokerName());
+                                KeyValue kv = new DefaultKeyValue();
+                                kv.put("queueId",msg.getQueueId());
+                                connectRecord.addExtension(kv);

Review Comment:
   Compatibility: Uses `io.openmessaging.internal.DefaultKeyValue`, an internal 
package not part of the public OMS API. This may break in future dependency 
versions. Additionally, verify that `connectRecord.addExtension(KeyValue)` 
properly merges the int-typed queueId value and that 
`getExtensions().getInt("queueId")` in WorkerSourceTask can round-trip it, 
since topic and brokerName are added as String extensions but queueId is added 
via a KeyValue with an int.



##########
connectors/rocketmq-replicator/src/main/java/org/apache/rocketmq/replicator/common/Utils.java:
##########
@@ -194,14 +196,30 @@ public static DefaultMQAdminExt startTargetMQAdminTool(
     }
 
     public static DefaultMQAdminExt startMQAdminTool(TaskConfig taskConfig) 
throws MQClientException {
+        RPCHook rpcHook = null;
+        if (taskConfig.isSrcAclEnable()) {
+            rpcHook = new AclClientRPCHook(new 
SessionCredentials(taskConfig.getSrcAccessKey(), taskConfig.getSrcSecretKey()));
+        }
+        DefaultMQAdminExt sourceMQAdminExt = new DefaultMQAdminExt(rpcHook);
+        sourceMQAdminExt.setNamesrvAddr(taskConfig.getSourceRocketmq());
+        
sourceMQAdminExt.setAdminExtGroup(ConstDefine.REPLICATOR_TASK_ADMIN_GROUP);
+        
sourceMQAdminExt.setInstanceName(Utils.createUniqInstanceName(taskConfig.getSourceRocketmq()));
+
+        sourceMQAdminExt.start();
+        log.info("Source: RocketMQ sourceMQAdminExt started.");
+
+        return sourceMQAdminExt;
+    }
+
+    public static DefaultMQAdminExt startTarMQAdminTool(TaskConfig taskConfig) 
throws MQClientException {

Review Comment:
   startTarMQAdminTool uses source ACL credentials (`isSrcAclEnable()`, 
`getSrcAccessKey()`, `getSrcSecretKey()`) for the target cluster admin tool. If 
the target cluster has ACL enabled with different credentials than the source, 
authentication will fail. TaskConfig should have separate target ACL config 
fields, or at minimum this limitation should be documented.



##########
rocketmq-connect-runtime/src/main/java/org/apache/rocketmq/connect/runtime/connectorwrapper/WorkerSourceTask.java:
##########
@@ -307,7 +312,53 @@ private void sendRecord() throws InterruptedException, 
RemotingException, MQClie
                         log.error("Send record, message size is greater than 
{} bytes, sourceDataEntry: {}", RuntimeConfigDefine.MAX_MESSAGE_SIZE, 
JSON.toJSONString(sourceDataEntry));
                         continue;
                     }
+                    String targetTopic = sourceDataEntry.getExtension("topic");
+                    if (targetTopic != null){
+                        sourceMessage.setTopic(targetTopic);
+                    }
                     sourceMessage.setBody(messageBody);
+                    int queueId = 
sourceDataEntry.getExtensions().getInt("queueId");
+                    String brokerName = 
sourceDataEntry.getExtension("brokerName");
+                    MessageQueue mq = new 
MessageQueue(targetTopic,brokerName,queueId);

Review Comment:
   Correctness: `new MessageQueue(targetTopic, brokerName, queueId)` is 
constructed unconditionally, but targetTopic and brokerName may be null — the 
null check at line 316 only guards `sourceMessage.setTopic()`, not the 
MessageQueue construction. Sending to a MessageQueue with null topic or broker 
name will fail at the broker. Validate these fields before entering the 
RocketMQConverter branch or ensure they are always set by the source task.



##########
connectors/rocketmq-replicator/src/main/java/org/apache/rocketmq/replicator/MetaSourceTask.java:
##########
@@ -120,28 +128,38 @@ public void resume() {
         List<ConnectRecord> res = new ArrayList<>();

Review Comment:
   No test coverage for the substantially changed meta offset sync logic 
(direct updateConsumeOffset, new startTarMQAdminTool, ClusterInfo traversal) or 
the RocketMQConverter send-to-specific-queue path in WorkerSourceTask. These 
are significant behavioral changes that warrant integration tests, especially 
given the NPE risks identified above.



##########
rocketmq-connect-runtime/src/main/java/org/apache/rocketmq/connect/runtime/connectorwrapper/WorkerSourceTask.java:
##########
@@ -265,6 +267,9 @@ public void cleanup() {
      */
     private void sendRecord() throws InterruptedException, RemotingException, 
MQClientException {
         for (ConnectRecord sourceDataEntry : toSendRecord) {
+            if (recordConverter instanceof RocketMQMetaConverter){

Review Comment:
   The early `return` (not `continue`) for RocketMQMetaConverter skips the 
`toSendRecord = null` cleanup at the end of sendRecord(). If the framework does 
not reset toSendRecord each cycle, records could accumulate across poll cycles 
for meta tasks. Use `continue` or ensure toSendRecord is cleared outside this 
method for meta tasks.



##########
rocketmq-connect-runtime/src/main/java/org/apache/rocketmq/connect/runtime/connectorwrapper/WorkerSourceTask.java:
##########
@@ -307,7 +312,53 @@ private void sendRecord() throws InterruptedException, 
RemotingException, MQClie
                         log.error("Send record, message size is greater than 
{} bytes, sourceDataEntry: {}", RuntimeConfigDefine.MAX_MESSAGE_SIZE, 
JSON.toJSONString(sourceDataEntry));
                         continue;
                     }
+                    String targetTopic = sourceDataEntry.getExtension("topic");

Review Comment:
   Maintainability: The SendCallback implementation (onSuccess/onException with 
stats tracking and position storage) is duplicated verbatim between the 
RocketMQConverter branch and the else branch (~40 lines each). Extract this 
into a helper method to avoid divergence bugs when one copy is updated but not 
the other.



##########
connectors/rocketmq-replicator/src/main/java/org/apache/rocketmq/replicator/MetaSourceTask.java:
##########
@@ -92,6 +99,7 @@ public void stop() {
             started = false;
         }
         srcMQAdminExt.shutdown();
+        tarMQAdminExt.shutdown();
     }
 

Review Comment:
   NPE risk in stop(): `tarMQAdminExt.shutdown()` is called unconditionally. If 
start() fails after creating srcMQAdminExt but before assigning tarMQAdminExt 
(e.g., startTarMQAdminTool throws MQClientException), stop() will throw NPE on 
a null tarMQAdminExt. Should null-check before calling shutdown.



##########
connectors/rocketmq-replicator/src/main/java/org/apache/rocketmq/replicator/MetaSourceTask.java:
##########
@@ -120,28 +128,38 @@ public void resume() {
         List<ConnectRecord> res = new ArrayList<>();
         for (String group : groups) {
             ConsumeStats stats;
+            String brokerAddresMaster="";
+            String brokerName="";
             try {
                 stats = this.srcMQAdminExt.examineConsumeStats(group);
+                ClusterInfo clusterInfo = 
this.tarMQAdminExt.examineBrokerClusterInfo();
+                HashMap<String, Set<String>> clusterAddrTable = 
clusterInfo.getClusterAddrTable();
+                HashMap<String, BrokerData> brokerAddrTable = 
clusterInfo.getBrokerAddrTable();
+                Set<String> clusterNameSet = 
clusterAddrTable.get(this.config.getTargetCluster());
+                Iterator<String> it = clusterNameSet.iterator();
+                while (it.hasNext()){
+                   String clusterName = it.next();
+                   BrokerData brokerData = brokerAddrTable.get(clusterName);
+                   HashMap<Long, String> brokerAddrs = 
brokerData.getBrokerAddrs();
+                   brokerAddresMaster = brokerAddrs.get(new Long(0));
+                   brokerName = brokerData.getBrokerName();
+                    for (Map.Entry<MessageQueue, OffsetWrapper> offsetTable : 
stats.getOffsetTable().entrySet()) {
+                        MessageQueue mq = offsetTable.getKey();
+                        long srcOffset = 
offsetTable.getValue().getConsumerOffset();
+                        long targetOffset = this.store.convertTargetOffset(mq, 
group, srcOffset);
+                        try{
+                           if (brokerName.equals(mq.getBrokerName())){
+                               
this.tarMQAdminExt.updateConsumeOffset(brokerAddresMaster,group,mq,targetOffset);
+                           }
+                        }catch (Exception e){
+                            log.error("admin update consumer offset err", e);
+                        }
+                    }
+                }
             } catch (Exception e) {
                 log.error("admin get consumer info failed for consumer groups: 
" + group, e);
                 continue;
             }
-
-            for (Map.Entry<MessageQueue, OffsetWrapper> offsetTable : 
stats.getOffsetTable().entrySet()) {
-                MessageQueue mq = offsetTable.getKey();
-                long srcOffset = offsetTable.getValue().getConsumerOffset();
-                long targetOffset = this.store.convertTargetOffset(mq, group, 
srcOffset);
-
-                List<Field> fields = new ArrayList<Field>();
-                Schema schema = new Schema(SchemaEnum.OFFSET.name(), 
FieldType.INT64, fields);
-                schema.getFields().add(new Field(0, FieldName.OFFSET.getKey(), 
SchemaBuilder.string().build()));
-
-                JSONObject jsonObject = new JSONObject();
-                jsonObject.put(FieldName.OFFSET.getKey(), targetOffset);
-                ConnectRecord connectRecord = new 
ConnectRecord(Utils.offsetKey(mq),
-                    Utils.offsetValue(srcOffset), System.currentTimeMillis(), 
schema, jsonObject.toJSONString());
-                res.add(connectRecord);
-            }
         }
         return res;
     }

Review Comment:
   Offset tracking concern: poll() now always returns an empty ConnectRecord 
list, and sendRecord() returns early for RocketMQMetaConverter, bypassing 
positionStorageWriter entirely. The framework's position/offset tracking is 
completely skipped for meta tasks. On task restart or reassignment, no progress 
is recorded. Verify this is intentional — the direct updateConsumeOffset 
approach may need at-least-once delivery guarantees that the framework no 
longer provides for this path.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to