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


##########
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:
   Queue routing is applied unconditionally to every record in the `null == 
recordConverter || recordConverter instanceof RocketMQConverter` branch. 
ConnectRecord.getExtensions() returns null unless addExtension() was called 
(putExtendMsgProperty in this same method null-checks it), so 
`sourceDataEntry.getExtensions().getInt("queueId")` throws NPE for any record 
without extensions. Even when extensions exist but lack 
queueId/brokerName/topic, DefaultKeyValue.getInt() returns 0 and 
brokerName/targetTopic are null, so `new MessageQueue(null, null, 0)` is passed 
to producer.send() and the send fails. This is shared runtime code: every 
existing source connector that sets no converter or uses RocketMQConverter (not 
just the replicator) breaks. Guard this block (only route to a MessageQueue 
when topic+brokerName+queueId are all present) and fall back to 
producer.send(sourceMessage) otherwise.



##########
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 RocketMQMetaConverter early-return sits inside the per-record loop and 
skips the `toSendRecord = null` cleanup at the end of sendRecord(). In run(), 
poll() is only invoked when toSendRecord is empty, so any meta-converter task 
that returns at least one record would busy-spin forever (100% CPU, no further 
polls, no position commits). Today MetaSourceTask.poll() always returns an 
empty list so this is dead code, but the check is converter-scoped, not 
record-scoped — move it before the loop (or clear the list) so the invariant 
can't be broken by a future task that emits records with this converter.



##########
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())){

Review Comment:
   Offset sync matches the TARGET broker name against the SOURCE MessageQueue 
broker name, and WorkerSourceTask similarly sends to the same 
queueId/brokerName — the whole change assumes the target cluster has identical 
broker names and at least as many queues per topic. When names or queue counts 
differ, offsets are silently skipped here and data sends throw 
MQClientException in the runtime (records dropped). Also brokerAddrs.get(new 
Long(0)) returns null when no master is registered, making updateConsumeOffset 
fail. Validate the target route exists and log a clear warning otherwise; use 
Long.valueOf(0)/MixAll.MASTER_ID instead of new Long(0).



##########
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:
   examineBrokerClusterInfo() is an expensive admin RPC invoked inside the 
per-group loop on every poll, and poll() is called in a tight runtime loop with 
no sleep, so cluster info is re-fetched G times per iteration. Hoist the call 
outside the groups loop and cache it (or refresh periodically); the nested 
group × broker × queue iteration can also be inverted so each queue is matched 
against a broker-name map.



##########
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();

Review Comment:
   `clusterAddrTable.get(this.config.getTargetCluster())` returns null when 
target-cluster is not configured (older connector configs; DefaultKeyValue.put 
even stores null as the string "null") and `clusterNameSet.iterator()` NPEs. 
The broad catch swallows it with the misleading message "admin get consumer 
info failed" and offset sync silently does nothing — an operational trap. 
Validate targetCluster/targetRocketmq in validate()/start() and fail fast, and 
null-check the lookup with an explicit error.



##########
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());

Review Comment:
   Adding routing metadata as plain extensions corrupts replicated messages: 
original message properties are copied to extensions first (line 181), then 
addExtension("topic"/"brokerName"/"queueId") overwrites any same-named user 
property, silently losing source data. Additionally, putExtendMsgProperty 
writes all extensions back as message properties (connect-ext-topic, 
connect-ext-brokerName, connect-ext-queueId), so every replicated message gains 
three properties the original never had. Use a dedicated prefix (e.g. 
connect-internal-*) or a separate mechanism for routing metadata instead of the 
generic "topic" key, which also risks hijacking the routing of any other 
connector whose records happen to carry a "topic" extension.



##########
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:
   stop() calls tarMQAdminExt.shutdown() without a null guard. If start() 
throws after srcMQAdminExt started (e.g. startTarMQAdminTool failure), 
tarMQAdminExt is null and stop() NPEs, masking the real error, while the 
already-started srcMQAdminExt is never shut down (resource leak). Guard both 
shutdowns and release srcMQAdminExt when the target admin fails to start.



##########
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 {
         RPCHook rpcHook = null;
         if (taskConfig.isSrcAclEnable()) {

Review Comment:
   startTarMQAdminTool connects to taskConfig.getTargetRocketmq() but 
authenticates with SOURCE ACL credentials 
(isSrcAclEnable/getSrcAccessKey/getSrcSecretKey). TaskConfig has no target ACL 
fields even though RmqConnectorConfig does (see startTargetMQAdminTool using 
isTargetAclEnable), so with ACL enabled on the target cluster under different 
credentials every admin call fails. Also this method is a near-duplicate of 
startMQAdminTool/startTargetMQAdminTool — add target ACL fields to TaskConfig 
and reuse one implementation.



##########
rocketmq-connect-runtime/src/main/java/org/apache/rocketmq/connect/runtime/connectorwrapper/WorkerSourceTask.java:
##########
@@ -316,45 +367,45 @@ private void sendRecord() throws InterruptedException, 
RemotingException, MQClie
                     continue;
                 }
                 sourceMessage.setBody(messageBody);
-            }
-            try {
-                producer.send(sourceMessage, new SendCallback() {
-                    @Override public void 
onSuccess(org.apache.rocketmq.client.producer.SendResult result) {
-                        log.info("Successful send message to RocketMQ:{}, 
Topic {}", result.getMsgId(), result.getMessageQueue().getTopic());
-                        connectStatsManager.incSourceRecordWriteTotalNums();
-                        
connectStatsManager.incSourceRecordWriteNums(taskConfig.getString(RuntimeConfigDefine.TASK_ID));
-                        RecordPartition partition = position.getPartition();
-                        try {
-                            if (null != partition && null != position) {
-                                Map<String, String> offsetMap = (Map<String, 
String>) offset.getOffset();
-                                
offsetMap.put(RuntimeConfigDefine.UPDATE_TIMESTAMP, 
String.valueOf(sourceDataEntry.getTimestamp()));
-                                positionStorageWriter.putPosition(partition, 
offset);
+                try {

Review Comment:
   The async send/callback/error-handling block (~90 lines) is now duplicated 
between the RocketMQConverter branch and the JSON branch. Extract a shared 
send-with-callback helper taking the optional MessageQueue so fixes to 
offset-commit-on-success or failure stats don't have to be applied twice (the 
copy already risks divergence).



##########
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:
   poll() now always returns an empty list — the record/schema machinery 
(ConnectRecord, Schema, Field, SchemaBuilder, JSONObject, FieldName, SchemaEnum 
imports, and the `res` variable) is dead code and poll() is purely 
side-effecting. Note also that OffsetSyncStore.sync() is never invoked (its 
consumer is never started), so convertTargetOffset degenerates to identity and 
updateConsumeOffset copies source offsets verbatim — wrong if target offsets 
diverge (e.g. replication didn't start at offset 0). Clean up the dead code and 
document that offset.sync.topic is no longer used by this path.



##########
connectors/rocketmq-replicator/README.md:
##########
@@ -44,7 +44,7 @@ 
http://${runtime-ip}:${runtime-port}/connectors/${rocketmq-replicator-name}/stop
 注:此功能尚不成熟还需要后续版本优化
 ````
 http://${runtime-ip}:${runtime-port}/connectors/${rocketmq-replicator-name}
-?config={"connector-class":"org.apache.rocketmq.replicator.RmqMetaReplicator","source-rocketmq":"xxxx:9876","target-rocketmq":"xxxxxxx:9876","replicator-store-topic":"replicatorTopic","offset.sync.topic":"syncTopic","taskDivideStrategy":"0","white-list":"TopicTest,TopicTest2","task-parallelism":"2","source-record-converter":"org.apache.rocketmq.connect.runtime.converter.JsonConverter"}
+?config={"connector-class":"org.apache.rocketmq.replicator.RmqMetaReplicator","source-rocketmq":"xxxx:9876","target-rocketmq":"xxxxxxx:9876","target-cluster":"test1-rocketmq","source-cluster":"test1-rocketmq","replicator-store-topic":"replicatorTopic","offset.sync.topic":"syncTopic","taskDivideStrategy":"0","white-list":"TestGroup","task-parallelism":"2","source-record-converter":"org.apache.rocketmq.connect.runtime.converter.RocketMQMetaConverter"}

Review Comment:
   The meta replicator example sets source-cluster and target-cluster to the 
same value ("test1-rocketmq"), which is almost certainly a copy-paste mistake 
and confusing for users setting up cross-cluster replication; also the example 
still documents offset.sync.topic even though offset sync no longer flows 
through a topic (direct admin updates now).



-- 
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