Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions connectors/rocketmq-replicator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ mvn clean install -Prelease-all -DskipTest -U
同步topic和消息
````
http://${runtime-ip}:${runtime-port}/connectors/${rocketmq-replicator-name}
?config={"connector-class":"org.apache.rocketmq.replicator.RmqSourceReplicator","source-rocketmq":"xxxx:9876","target-rocketmq":"xxxxxxx:9876","replicator-store-topic":"replicatorTopic","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.RmqSourceReplicator","source-rocketmq":"xxxx:9876","target-rocketmq":"xxxxxxx:9876","replicator-store-topic":"replicatorTopic","taskDivideStrategy":"0","white-list":"TopicTest,TopicTest2","task-parallelism":"2","source-record-converter":"org.apache.rocketmq.connect.runtime.converter.RocketMQConverter"}
````


Expand All @@ -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"}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

````


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,16 @@
import java.util.Collections;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No test changes detected alongside source modifications. Consider adding tests to cover the changes.

import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.admin.ConsumeStats;
import org.apache.rocketmq.common.admin.OffsetWrapper;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.common.protocol.body.ClusterInfo;
import org.apache.rocketmq.common.protocol.route.BrokerData;
import org.apache.rocketmq.replicator.common.Utils;
import org.apache.rocketmq.replicator.config.ConfigUtil;
import org.apache.rocketmq.replicator.config.TaskConfig;
Expand All @@ -52,6 +57,7 @@ public class MetaSourceTask extends SourceTask {
private final String taskId;
private final TaskConfig config;
private DefaultMQAdminExt srcMQAdminExt;
private DefaultMQAdminExt tarMQAdminExt;
private volatile boolean started = false;

private OffsetSyncStore store;
Expand All @@ -77,6 +83,7 @@ public void start(SourceTaskContext sourceTaskContext) {

try {
this.srcMQAdminExt = Utils.startMQAdminTool(this.config);
this.tarMQAdminExt = Utils.startTarMQAdminTool(this.config);
} catch (MQClientException e) {
log.error("Replicator task start failed for `startMQAdminTool` exception.", e);
throw new IllegalStateException("Replicator task start failed for `startMQAdminTool` exception.");
Expand All @@ -92,6 +99,7 @@ public void stop() {
started = false;
}
srcMQAdminExt.shutdown();
tarMQAdminExt.shutdown();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Override
Expand Down Expand Up @@ -120,28 +128,38 @@ public void resume() {
List<ConnectRecord> res = new ArrayList<>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

for (String group : groups) {
ConsumeStats stats;
String brokerAddresMaster="";
String brokerName="";
try {
stats = this.srcMQAdminExt.examineConsumeStats(group);
ClusterInfo clusterInfo = this.tarMQAdminExt.examineBrokerClusterInfo();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

HashMap<String, Set<String>> clusterAddrTable = clusterInfo.getClusterAddrTable();
HashMap<String, BrokerData> brokerAddrTable = clusterInfo.getBrokerAddrTable();
Set<String> clusterNameSet = clusterAddrTable.get(this.config.getTargetCluster());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Iterator<String> it = clusterNameSet.iterator();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

while (it.hasNext()){

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

String clusterName = it.next();
BrokerData brokerData = brokerAddrTable.get(clusterName);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import io.openmessaging.KeyValue;
import io.openmessaging.internal.DefaultKeyValue;
import io.openmessaging.connector.api.component.task.source.SourceTask;
import io.openmessaging.connector.api.component.task.source.SourceTaskContext;
import io.openmessaging.connector.api.data.ConnectRecord;
Expand Down Expand Up @@ -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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

connectRecord.addExtension("brokerName",msg.getBrokerName());
KeyValue kv = new DefaultKeyValue();
kv.put("queueId",msg.getQueueId());
connectRecord.addExtension(kv);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

res.add(connectRecord);
}
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ public static List<KeyValue> groupPartitions(List<String> elements, RmqConnector
assigned++;
}
keyValue.put(TaskConfigEnum.TASK_STORE_ROCKETMQ.getKey(), tdc.getStoreTopic());
keyValue.put(TaskConfigEnum.TASK_TARGET_ROCKETMQ.getKey(), tdc.getTargetNamesrvs());
keyValue.put(TaskConfigEnum.TASK_SOURCE_ROCKETMQ.getKey(), tdc.getSrcNamesrvs());
keyValue.put(TaskConfigEnum.TASK_TARGET_CLUSTER.getKey(), tdc.getTargetCluster());
keyValue.put(TaskConfigEnum.TASK_SOURCE_CLUSTER.getKey(), tdc.getSrcCluster());
keyValue.put(TaskConfigEnum.TASK_OFFSET_SYNC_TOPIC.getKey(), tdc.getOffsetSyncTopic());
keyValue.put(TaskConfigEnum.TASK_DATA_TYPE.getKey(), DataType.OFFSET.ordinal());
Expand Down Expand Up @@ -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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

RPCHook rpcHook = null;
if (taskConfig.isSrcAclEnable()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

rpcHook = new AclClientRPCHook(new SessionCredentials(taskConfig.getSrcAccessKey(), taskConfig.getSrcSecretKey()));
}
DefaultMQAdminExt targetMQAdminExt = new DefaultMQAdminExt(rpcHook);
targetMQAdminExt.setNamesrvAddr(taskConfig.getSourceRocketmq());
targetMQAdminExt.setNamesrvAddr(taskConfig.getTargetRocketmq());
targetMQAdminExt.setAdminExtGroup(ConstDefine.REPLICATOR_TASK_ADMIN_GROUP);
targetMQAdminExt.setInstanceName(Utils.createUniqInstanceName(taskConfig.getSourceRocketmq()));
targetMQAdminExt.setInstanceName(Utils.createUniqInstanceName(taskConfig.getTargetRocketmq()));

targetMQAdminExt.start();
log.info("TARGET: RocketMQ targetMQAdminExt started.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
public class TaskConfig {

private String sourceCluster;
private String targetCluster;
private String storeTopic;
private String sourceGroup;
private String sourceRocketmq;
private String targetRocketmq;
private Integer dataType;
private Long nextPosition;
private String taskTopicList;
Expand Down Expand Up @@ -55,6 +57,14 @@ public void setSourceRocketmq(String sourceRocketmq) {
this.sourceRocketmq = sourceRocketmq;
}

public String getTargetRocketmq() {
return targetRocketmq;
}

public void setTargetRocketmq(String targetRocketmq) {
this.targetRocketmq = targetRocketmq;
}

public int getDataType() {
return dataType;
}
Expand Down Expand Up @@ -99,6 +109,14 @@ public String getSourceCluster() {
return this.sourceCluster;
}

public void setTargetCluster(String targetCluster) {
this.targetCluster = targetCluster;
}

public String getTargetCluster() {
return this.targetCluster;
}

public void setOffsetSyncTopic(String offsetSyncTopic) {
this.offsetSyncTopic = offsetSyncTopic;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public enum TaskConfigEnum {
TASK_SOURCE_GROUP("sourceGroup"),
TASK_SOURCE_ROCKETMQ("sourceRocketmq"),
TASK_SOURCE_CLUSTER("sourceCluster"),
TASK_TARGET_ROCKETMQ("targetRocketmq"),
TASK_TARGET_CLUSTER("targetCluster"),
TASK_OFFSET_SYNC_TOPIC("offsetSyncTopic"),
TASK_SOURCE_TOPIC("sourceTopic"),
TASK_STORE_ROCKETMQ("storeTopic"),
Expand Down
Loading