This is an automated email from the ASF dual-hosted git repository.

rong pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/master by this push:
     new 7e5b7516724 Pipe: implement external sources strategy and MQTT 
extractor  (#15275)
7e5b7516724 is described below

commit 7e5b75167241ac44ced008eec9d81db8bb4bd24f
Author: nanxiang xia <[email protected]>
AuthorDate: Fri May 16 11:13:29 2025 +0800

    Pipe: implement external sources strategy and MQTT extractor  (#15275)
    
    Co-authored-by: Steve Yurong Su <[email protected]>
---
 .../confignode/persistence/pipe/PipeTaskInfo.java  |  11 +
 .../impl/pipe/AbstractOperatePipeProcedureV2.java  |   4 +
 .../impl/pipe/runtime/PipeMetaSyncProcedure.java   |  67 ++++-
 .../impl/pipe/task/AlterPipeProcedureV2.java       |  78 ++---
 .../impl/pipe/task/CreatePipeProcedureV2.java      |  29 ++
 .../pipe/util/PipeExternalSourceLoadBalancer.java  | 218 ++++++++++++++
 .../pipe/agent/plugin/PipeDataNodePluginAgent.java |  15 +
 .../PipeDataRegionExtractorConstructor.java        |   5 +
 .../db/pipe/agent/task/PipeDataNodeTaskAgent.java  |  12 +-
 .../agent/task/builder/PipeDataNodeBuilder.java    |  20 +-
 .../task/builder/PipeDataNodeTaskBuilder.java      |  30 ++
 .../agent/task/stage/PipeTaskExtractorStage.java   |   2 +
 .../agent/task/stage/PipeTaskProcessorStage.java   |   2 +
 .../connector/PipeConnectorSubtaskManager.java     |   6 +-
 .../subtask/processor/PipeProcessorSubtask.java    |   4 +-
 .../protocol/writeback/WriteBackConnector.java     |  82 ++++-
 .../statement/PipeStatementInsertionEvent.java     | 222 ++++++++++++++
 .../common/tablet/PipeRawTabletInsertionEvent.java |  33 +++
 .../db/pipe/extractor/mqtt/MQTTExtractor.java      | 182 ++++++++++++
 .../db/pipe/extractor/mqtt/MQTTPublishHandler.java | 330 +++++++++++++++++++++
 .../db/protocol/mqtt/JSONPayloadFormatter.java     |  13 +
 .../config/executor/ClusterConfigTaskExecutor.java |  36 +++
 .../agent/plugin/builtin/BuiltinPipePlugin.java    |  13 +
 .../builtin/extractor/mqtt/MQTTExtractor.java      |  68 +++++
 .../pipe/agent/task/meta/PipeRuntimeMeta.java      |  10 +
 .../pipe/agent/task/meta/PipeStaticMeta.java       |  17 +-
 .../config/constant/PipeConnectorConstant.java     |   4 +
 .../config/constant/PipeExtractorConstant.java     |  50 ++++
 28 files changed, 1510 insertions(+), 53 deletions(-)

diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/pipe/PipeTaskInfo.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/pipe/PipeTaskInfo.java
index 44562aa8bbb..30333c66768 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/pipe/PipeTaskInfo.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/pipe/PipeTaskInfo.java
@@ -594,6 +594,17 @@ public class PipeTaskInfo implements SnapshotProcessor {
                             return; // pipe consensus pipe task will not change
                           }
 
+                          if (pipeMeta.getStaticMeta().isSourceExternal()) {
+                            // external source pipe tasks are not balanced 
here since non-leaders
+                            // don't know about RegionLeader Map and will be 
balanced in the meta
+                            // sync procedure
+                            LOGGER.info(
+                                "Pipe {} is using external source, skip region 
leader change. PipeHandleLeaderChangePlan: {}",
+                                pipeMeta.getStaticMeta().getPipeName(),
+                                plan.getConsensusGroupId2NewLeaderIdMap());
+                            return;
+                          }
+
                           final Map<Integer, PipeTaskMeta> 
consensusGroupIdToTaskMetaMap =
                               
pipeMeta.getRuntimeMeta().getConsensusGroupId2TaskMetaMap();
 
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/AbstractOperatePipeProcedureV2.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/AbstractOperatePipeProcedureV2.java
index 0e5ba5bf9dd..d8b03991a4e 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/AbstractOperatePipeProcedureV2.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/AbstractOperatePipeProcedureV2.java
@@ -520,6 +520,10 @@ public abstract class AbstractOperatePipeProcedureV2
         .entrySet()
         .removeIf(
             consensusGroupId2TaskMeta -> {
+              if (originalPipeMeta.getStaticMeta().isSourceExternal()) {
+                // should keep the external source tasks
+                return false;
+              }
               final String database;
               try {
                 database =
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/runtime/PipeMetaSyncProcedure.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/runtime/PipeMetaSyncProcedure.java
index c8a734a9e35..43b1660681e 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/runtime/PipeMetaSyncProcedure.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/runtime/PipeMetaSyncProcedure.java
@@ -20,15 +20,20 @@
 package org.apache.iotdb.confignode.procedure.impl.pipe.runtime;
 
 import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.consensus.index.impl.MinimumProgressIndex;
 import org.apache.iotdb.commons.pipe.agent.task.meta.PipeMeta;
+import org.apache.iotdb.commons.pipe.agent.task.meta.PipeTaskMeta;
 import org.apache.iotdb.commons.pipe.config.PipeConfig;
+import org.apache.iotdb.commons.pipe.config.constant.PipeExtractorConstant;
 import 
org.apache.iotdb.confignode.consensus.request.write.pipe.runtime.PipeHandleMetaChangePlan;
 import org.apache.iotdb.confignode.persistence.pipe.PipeTaskInfo;
 import org.apache.iotdb.confignode.procedure.env.ConfigNodeProcedureEnv;
 import 
org.apache.iotdb.confignode.procedure.impl.pipe.AbstractOperatePipeProcedureV2;
 import org.apache.iotdb.confignode.procedure.impl.pipe.PipeTaskOperation;
+import 
org.apache.iotdb.confignode.procedure.impl.pipe.util.PipeExternalSourceLoadBalancer;
 import org.apache.iotdb.confignode.procedure.state.ProcedureLockState;
 import org.apache.iotdb.confignode.procedure.store.ProcedureType;
+import org.apache.iotdb.confignode.service.ConfigNode;
 import org.apache.iotdb.consensus.exception.ConsensusException;
 import org.apache.iotdb.mpp.rpc.thrift.TPushPipeMetaResp;
 import org.apache.iotdb.pipe.api.exception.PipeException;
@@ -40,10 +45,17 @@ import org.slf4j.LoggerFactory;
 import java.io.DataOutputStream;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+
+import static 
org.apache.iotdb.commons.pipe.config.constant.PipeExtractorConstant.EXTERNAL_EXTRACTOR_PARALLELISM_DEFAULT_VALUE;
+import static 
org.apache.iotdb.commons.pipe.config.constant.PipeExtractorConstant.EXTERNAL_EXTRACTOR_PARALLELISM_KEY;
+import static 
org.apache.iotdb.commons.pipe.config.constant.PipeExtractorConstant.EXTERNAL_SOURCE_PARALLELISM_KEY;
 
 public class PipeMetaSyncProcedure extends AbstractOperatePipeProcedureV2 {
 
@@ -101,7 +113,60 @@ public class PipeMetaSyncProcedure extends 
AbstractOperatePipeProcedureV2 {
   public void executeFromCalculateInfoForTask(ConfigNodeProcedureEnv env) {
     LOGGER.info("PipeMetaSyncProcedure: executeFromCalculateInfoForTask");
 
-    // Do nothing
+    // Re-balance the external source tasks here in case of any changes in the 
dataRegion
+    pipeTaskInfo
+        .get()
+        .getPipeMetaList()
+        .forEach(
+            pipeMeta -> {
+              if (!pipeMeta.getStaticMeta().isSourceExternal()) {
+                return;
+              }
+
+              final PipeExternalSourceLoadBalancer loadBalancer =
+                  new PipeExternalSourceLoadBalancer(
+                      pipeMeta
+                          .getStaticMeta()
+                          .getExtractorParameters()
+                          .getStringOrDefault(
+                              Arrays.asList(
+                                  
PipeExtractorConstant.EXTERNAL_EXTRACTOR_BALANCE_STRATEGY_KEY,
+                                  
PipeExtractorConstant.EXTERNAL_SOURCE_BALANCE_STRATEGY_KEY),
+                              PipeExtractorConstant
+                                  
.EXTERNAL_EXTRACTOR_BALANCE_PROPORTION_STRATEGY));
+              final int parallelism =
+                  pipeMeta
+                      .getStaticMeta()
+                      .getExtractorParameters()
+                      .getIntOrDefault(
+                          Arrays.asList(
+                              EXTERNAL_EXTRACTOR_PARALLELISM_KEY, 
EXTERNAL_SOURCE_PARALLELISM_KEY),
+                          EXTERNAL_EXTRACTOR_PARALLELISM_DEFAULT_VALUE);
+              final Map<Integer, PipeTaskMeta> consensusGroupIdToTaskMetaMap =
+                  pipeMeta.getRuntimeMeta().getConsensusGroupId2TaskMetaMap();
+
+              // do balance here
+              final Map<Integer, Integer> taskId2LeaderDataNodeId =
+                  loadBalancer.balance(
+                      parallelism,
+                      pipeMeta.getStaticMeta(),
+                      ConfigNode.getInstance().getConfigManager());
+
+              taskId2LeaderDataNodeId.forEach(
+                  (taskIndex, newLeader) -> {
+                    if (consensusGroupIdToTaskMetaMap.containsKey(taskIndex)) {
+                      
consensusGroupIdToTaskMetaMap.get(taskIndex).setLeaderNodeId(newLeader);
+                    } else {
+                      consensusGroupIdToTaskMetaMap.put(
+                          taskIndex, new 
PipeTaskMeta(MinimumProgressIndex.INSTANCE, newLeader));
+                    }
+                  });
+              final Set<Integer> taskIdToRemove =
+                  consensusGroupIdToTaskMetaMap.keySet().stream()
+                      .filter(taskId -> 
!taskId2LeaderDataNodeId.containsKey(taskId))
+                      .collect(Collectors.toSet());
+              taskIdToRemove.forEach(consensusGroupIdToTaskMetaMap::remove);
+            });
   }
 
   @Override
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/task/AlterPipeProcedureV2.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/task/AlterPipeProcedureV2.java
index 8b59759e38b..a1305e99d28 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/task/AlterPipeProcedureV2.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/task/AlterPipeProcedureV2.java
@@ -162,40 +162,50 @@ public class AlterPipeProcedureV2 extends 
AbstractOperatePipeProcedureV2 {
 
     final ConcurrentMap<Integer, PipeTaskMeta> 
updatedConsensusGroupIdToTaskMetaMap =
         new ConcurrentHashMap<>();
-    // data regions & schema regions
-    env.getConfigManager()
-        .getLoadManager()
-        .getRegionLeaderMap()
-        .forEach(
-            (regionGroupId, regionLeaderNodeId) -> {
-              final String databaseName =
-                  
env.getConfigManager().getPartitionManager().getRegionDatabase(regionGroupId);
-              final PipeTaskMeta currentPipeTaskMeta =
-                  
currentConsensusGroupId2PipeTaskMeta.get(regionGroupId.getId());
-              if (databaseName != null
-                  && !databaseName.equals(SchemaConstant.SYSTEM_DATABASE)
-                  && !databaseName.startsWith(SchemaConstant.SYSTEM_DATABASE + 
".")
-                  && currentPipeTaskMeta != null
-                  && currentPipeTaskMeta.getLeaderNodeId() == 
regionLeaderNodeId) {
-                // Pipe only collect user's data, filter metric database here.
-                updatedConsensusGroupIdToTaskMetaMap.put(
-                    regionGroupId.getId(),
-                    new PipeTaskMeta(currentPipeTaskMeta.getProgressIndex(), 
regionLeaderNodeId));
-              }
-            });
-
-    final PipeTaskMeta configRegionTaskMeta =
-        currentConsensusGroupId2PipeTaskMeta.get(Integer.MIN_VALUE);
-    if (Objects.nonNull(configRegionTaskMeta)) {
-      // config region
-      updatedConsensusGroupIdToTaskMetaMap.put(
-          // 0 is the consensus group id of the config region, but data region 
id and schema region
-          // id also start from 0, so we use Integer.MIN_VALUE to represent 
the config region
-          Integer.MIN_VALUE,
-          new PipeTaskMeta(
-              configRegionTaskMeta.getProgressIndex(),
-              // The leader of the config region is the config node itself
-              ConfigNodeDescriptor.getInstance().getConf().getConfigNodeId()));
+    if (currentPipeStaticMeta.isSourceExternal()) {
+      currentConsensusGroupId2PipeTaskMeta.forEach(
+          (taskId, pipeTaskMeta) -> {
+            updatedConsensusGroupIdToTaskMetaMap.put(
+                taskId,
+                new PipeTaskMeta(pipeTaskMeta.getProgressIndex(), 
pipeTaskMeta.getLeaderNodeId()));
+          });
+    } else {
+      // data regions & schema regions
+      env.getConfigManager()
+          .getLoadManager()
+          .getRegionLeaderMap()
+          .forEach(
+              (regionGroupId, regionLeaderNodeId) -> {
+                final String databaseName =
+                    
env.getConfigManager().getPartitionManager().getRegionDatabase(regionGroupId);
+                final PipeTaskMeta currentPipeTaskMeta =
+                    
currentConsensusGroupId2PipeTaskMeta.get(regionGroupId.getId());
+                if (databaseName != null
+                    && !databaseName.equals(SchemaConstant.SYSTEM_DATABASE)
+                    && !databaseName.startsWith(SchemaConstant.SYSTEM_DATABASE 
+ ".")
+                    && currentPipeTaskMeta != null
+                    && currentPipeTaskMeta.getLeaderNodeId() == 
regionLeaderNodeId) {
+                  // Pipe only collect user's data, filter metric database 
here.
+                  updatedConsensusGroupIdToTaskMetaMap.put(
+                      regionGroupId.getId(),
+                      new PipeTaskMeta(currentPipeTaskMeta.getProgressIndex(), 
regionLeaderNodeId));
+                }
+              });
+
+      final PipeTaskMeta configRegionTaskMeta =
+          currentConsensusGroupId2PipeTaskMeta.get(Integer.MIN_VALUE);
+      if (Objects.nonNull(configRegionTaskMeta)) {
+        // config region
+        updatedConsensusGroupIdToTaskMetaMap.put(
+            // 0 is the consensus group id of the config region, but data 
region id and schema
+            // region id also start from 0, so we use Integer.MIN_VALUE to 
represent the config
+            // region
+            Integer.MIN_VALUE,
+            new PipeTaskMeta(
+                configRegionTaskMeta.getProgressIndex(),
+                // The leader of the config region is the config node itself
+                
ConfigNodeDescriptor.getInstance().getConf().getConfigNodeId()));
+      }
     }
 
     updatedPipeRuntimeMeta = new 
PipeRuntimeMeta(updatedConsensusGroupIdToTaskMetaMap);
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/task/CreatePipeProcedureV2.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/task/CreatePipeProcedureV2.java
index 5b4dcb0e1c1..130d964f240 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/task/CreatePipeProcedureV2.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/task/CreatePipeProcedureV2.java
@@ -42,6 +42,7 @@ import 
org.apache.iotdb.confignode.persistence.pipe.PipeTaskInfo;
 import org.apache.iotdb.confignode.procedure.env.ConfigNodeProcedureEnv;
 import 
org.apache.iotdb.confignode.procedure.impl.pipe.AbstractOperatePipeProcedureV2;
 import org.apache.iotdb.confignode.procedure.impl.pipe.PipeTaskOperation;
+import 
org.apache.iotdb.confignode.procedure.impl.pipe.util.PipeExternalSourceLoadBalancer;
 import org.apache.iotdb.confignode.procedure.store.ProcedureType;
 import org.apache.iotdb.confignode.rpc.thrift.TCreatePipeReq;
 import org.apache.iotdb.consensus.exception.ConsensusException;
@@ -66,6 +67,9 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.atomic.AtomicReference;
 
+import static 
org.apache.iotdb.commons.pipe.config.constant.PipeExtractorConstant.EXTERNAL_EXTRACTOR_PARALLELISM_DEFAULT_VALUE;
+import static 
org.apache.iotdb.commons.pipe.config.constant.PipeExtractorConstant.EXTERNAL_EXTRACTOR_PARALLELISM_KEY;
+import static 
org.apache.iotdb.commons.pipe.config.constant.PipeExtractorConstant.EXTERNAL_SOURCE_PARALLELISM_KEY;
 import static 
org.apache.iotdb.commons.pipe.config.constant.PipeExtractorConstant.EXTRACTOR_CONSENSUS_GROUP_ID_KEY;
 import static 
org.apache.iotdb.commons.pipe.config.constant.PipeExtractorConstant.EXTRACTOR_CONSENSUS_SENDER_DATANODE_ID_KEY;
 
@@ -269,6 +273,31 @@ public class CreatePipeProcedureV2 extends 
AbstractOperatePipeProcedureV2 {
           new PipeTaskMeta(
               new RecoverProgressIndex(senderDataNodeId, new 
SimpleProgressIndex(0, 0)),
               senderDataNodeId));
+    } else if (pipeStaticMeta.isSourceExternal()) {
+      // external source
+      final PipeExternalSourceLoadBalancer loadBalancer =
+          new PipeExternalSourceLoadBalancer(
+              pipeStaticMeta
+                  .getExtractorParameters()
+                  .getStringOrDefault(
+                      Arrays.asList(
+                          
PipeExtractorConstant.EXTERNAL_EXTRACTOR_BALANCE_STRATEGY_KEY,
+                          
PipeExtractorConstant.EXTERNAL_SOURCE_BALANCE_STRATEGY_KEY),
+                      
PipeExtractorConstant.EXTERNAL_EXTRACTOR_BALANCE_PROPORTION_STRATEGY));
+      final int parallelism =
+          pipeStaticMeta
+              .getExtractorParameters()
+              .getIntOrDefault(
+                  Arrays.asList(
+                      EXTERNAL_EXTRACTOR_PARALLELISM_KEY, 
EXTERNAL_SOURCE_PARALLELISM_KEY),
+                  EXTERNAL_EXTRACTOR_PARALLELISM_DEFAULT_VALUE);
+      loadBalancer
+          .balance(parallelism, pipeStaticMeta, env.getConfigManager())
+          .forEach(
+              (taskIndex, leaderNodeId) -> {
+                consensusGroupIdToTaskMetaMap.put(
+                    taskIndex, new PipeTaskMeta(MinimumProgressIndex.INSTANCE, 
leaderNodeId));
+              });
     } else {
       // data regions & schema regions
       env.getConfigManager()
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/util/PipeExternalSourceLoadBalancer.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/util/PipeExternalSourceLoadBalancer.java
new file mode 100644
index 00000000000..426cc6afbef
--- /dev/null
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/util/PipeExternalSourceLoadBalancer.java
@@ -0,0 +1,218 @@
+/*
+ * 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.iotdb.confignode.procedure.impl.pipe.util;
+
+import org.apache.iotdb.common.rpc.thrift.TConsensusGroupId;
+import org.apache.iotdb.common.rpc.thrift.TConsensusGroupType;
+import org.apache.iotdb.commons.cluster.NodeStatus;
+import org.apache.iotdb.commons.pipe.agent.task.meta.PipeStaticMeta;
+import org.apache.iotdb.commons.pipe.config.constant.PipeExtractorConstant;
+import org.apache.iotdb.confignode.manager.ConfigManager;
+import org.apache.iotdb.pipe.api.exception.PipeException;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * The PipeExternalSourceLoadBalancer is responsible for assigning parallel 
extraction tasks from an
+ * external source to available DataNodes in the cluster.
+ */
+public class PipeExternalSourceLoadBalancer {
+
+  /**
+   * The BalanceStrategy interface defines the contract for different load 
balancing strategies.
+   * Implementations of this interface should provide a way to balance tasks 
across DataNodes.
+   */
+  private interface BalanceStrategy {
+    Map<Integer, Integer> balance(
+        final int parallelCount,
+        final PipeStaticMeta pipeStaticMeta,
+        final ConfigManager configManager);
+  }
+
+  private final BalanceStrategy strategy;
+
+  public PipeExternalSourceLoadBalancer(final String balanceStrategy) {
+    switch (balanceStrategy) {
+      case 
PipeExtractorConstant.EXTERNAL_EXTRACTOR_BALANCE_PROPORTION_STRATEGY:
+        this.strategy = new ProportionalBalanceStrategy();
+        break;
+      default:
+        throw new IllegalArgumentException("Unknown load balance strategy: " + 
balanceStrategy);
+    }
+  }
+
+  /**
+   * Balances the given number of parallel tasks across available nodes.
+   *
+   * @param parallelCount number of external source tasks to distribute
+   * @param pipeStaticMeta metadata about the pipe extractor
+   * @param configManager reference to ConfigManager for cluster information
+   * @return a mapping from task index to leader node id
+   */
+  public Map<Integer, Integer> balance(
+      final int parallelCount,
+      final PipeStaticMeta pipeStaticMeta,
+      final ConfigManager configManager) {
+    return strategy.balance(parallelCount, pipeStaticMeta, configManager);
+  }
+
+  public static class ProportionalBalanceStrategy implements BalanceStrategy {
+
+    @Override
+    public Map<Integer, Integer> balance(
+        final int parallelCount,
+        final PipeStaticMeta pipeStaticMeta,
+        final ConfigManager configManager) {
+      final Map<TConsensusGroupId, Integer> regionLeaderMap =
+          configManager.getLoadManager().getRegionLeaderMap();
+      final Map<Integer, Integer> taskId2DataNodeId = new HashMap<>();
+
+      // Check for Single Instance Mode:
+      //
+      // 1. If the pipeStaticMeta indicates that only one instance per node is 
allowed, tasks are
+      // evenly distributed across running DataNodes.
+      // 2. If no DataNodes are available, a PipeException is thrown.
+      if (pipeStaticMeta
+          .getExtractorParameters()
+          .getBooleanOrDefault(
+              Arrays.asList(
+                  
PipeExtractorConstant.EXTERNAL_EXTRACTOR_SINGLE_INSTANCE_PER_NODE_KEY,
+                  
PipeExtractorConstant.EXTERNAL_SOURCE_SINGLE_INSTANCE_PER_NODE_KEY),
+              
PipeExtractorConstant.EXTERNAL_EXTRACTOR_SINGLE_INSTANCE_PER_NODE_DEFAULT_VALUE))
 {
+        final List<Integer> runningDataNodes =
+            
configManager.getLoadManager().filterDataNodeThroughStatus(NodeStatus.Running).stream()
+                .sorted()
+                .collect(Collectors.toList());
+        if (runningDataNodes.isEmpty()) {
+          throw new PipeException("No available datanode to assign tasks");
+        }
+        final int numNodes = runningDataNodes.size();
+        for (int i = 1; i <= Math.min(numNodes, parallelCount); i++) {
+          final int datanodeId = runningDataNodes.get(i - 1);
+          taskId2DataNodeId.put(-i, datanodeId);
+        }
+        return taskId2DataNodeId;
+      }
+
+      // Count DataRegions Led by Each DataNode:
+      //
+      // The method iterates through the regionLeaderMap to count the number 
of DataRegions led by
+      // each DataNode.
+      final Map<Integer, Integer> leaderRegionId2DataRegionCountMap = new 
HashMap<>();
+      regionLeaderMap.entrySet().stream()
+          .filter(e -> e.getKey().getType() == TConsensusGroupType.DataRegion 
&& e.getValue() != -1)
+          .forEach(
+              e -> {
+                final int leaderRegionDataNodeId = e.getValue();
+                leaderRegionId2DataRegionCountMap.put(
+                    leaderRegionDataNodeId,
+                    
leaderRegionId2DataRegionCountMap.getOrDefault(leaderRegionDataNodeId, 0) + 1);
+              });
+
+      // Handle No DataRegions:
+      //
+      // If no DataRegions exist, tasks are evenly distributed across running 
DataNodes.
+      if (leaderRegionId2DataRegionCountMap.isEmpty()) {
+        List<Integer> runningDataNodes =
+            
configManager.getLoadManager().filterDataNodeThroughStatus(NodeStatus.Running).stream()
+                .sorted()
+                .collect(Collectors.toList());
+        if (runningDataNodes.isEmpty()) {
+          throw new PipeException("No available datanode to assign tasks");
+        }
+        final int numNodes = runningDataNodes.size();
+        final int quotient = parallelCount / numNodes;
+        final int remainder = parallelCount % numNodes;
+        int taskIndex = 1;
+        for (int i = 0; i < numNodes; i++) {
+          int tasksForNode = quotient + (i < remainder ? 1 : 0);
+          int datanodeId = runningDataNodes.get(i);
+          for (int j = 0; j < tasksForNode; j++) {
+            taskId2DataNodeId.put(-taskIndex, datanodeId);
+            taskIndex++;
+          }
+        }
+        return taskId2DataNodeId;
+      }
+
+      // Proportional Task Distribution:
+      //
+      // Based on the number of DataRegions led by each DataNode, the method 
calculates the
+      // proportion of tasks each node should handle.
+      // Integer parts of the task count are assigned first, and remaining 
tasks are distributed
+      // based on the largest fractional parts.
+      final int totalRegions =
+          
leaderRegionId2DataRegionCountMap.values().stream().mapToInt(Integer::intValue).sum();
+
+      // Calculate exact and floor share of each leader
+      final Map<Integer, Double> leaderRegionId2ExactShareMap = new 
HashMap<>();
+      final Map<Integer, Integer> leaderRegionId2AssignedCountMap = new 
HashMap<>();
+      for (Map.Entry<Integer, Integer> entry : 
leaderRegionId2DataRegionCountMap.entrySet()) {
+        final double share = (parallelCount * entry.getValue()) / (double) 
totalRegions;
+        leaderRegionId2ExactShareMap.put(entry.getKey(), share);
+        leaderRegionId2AssignedCountMap.put(entry.getKey(), (int) 
Math.floor(share));
+      }
+
+      // Distribute remainder tasks based on largest fractional parts
+      final int remainder =
+          parallelCount
+              - 
leaderRegionId2AssignedCountMap.values().stream().mapToInt(Integer::intValue).sum();
+
+      final List<Integer> sortedLeaders =
+          leaderRegionId2ExactShareMap.keySet().stream()
+              .sorted(
+                  (l1, l2) -> {
+                    final double diff =
+                        (leaderRegionId2ExactShareMap.get(l2)
+                                - 
Math.floor(leaderRegionId2ExactShareMap.get(l2)))
+                            - (leaderRegionId2ExactShareMap.get(l1)
+                                - 
Math.floor(leaderRegionId2ExactShareMap.get(l1)));
+                    return diff > 0 ? 1 : (diff < 0 ? -1 : Integer.compare(l1, 
l2));
+                  })
+              .collect(Collectors.toList());
+      for (int i = 0; i < remainder; i++) {
+        final int leaderId = sortedLeaders.get(i % sortedLeaders.size());
+        leaderRegionId2AssignedCountMap.put(
+            leaderId, leaderRegionId2AssignedCountMap.get(leaderId) + 1);
+      }
+
+      // Generate Task Assignment Result:
+      //
+      // Finally, the method returns a mapping of task indices to DataNode IDs.
+      final List<Integer> stableLeaders = new 
ArrayList<>(leaderRegionId2AssignedCountMap.keySet());
+      Collections.sort(stableLeaders);
+      int taskIndex = 1;
+      for (final Integer leader : stableLeaders) {
+        final int count = leaderRegionId2AssignedCountMap.get(leader);
+        for (int i = 0; i < count; i++) {
+          taskId2DataNodeId.put(-taskIndex, leader);
+          taskIndex++;
+        }
+      }
+      return taskId2DataNodeId;
+    }
+  }
+}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/plugin/PipeDataNodePluginAgent.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/plugin/PipeDataNodePluginAgent.java
index 088d19966b4..11c1edafccb 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/plugin/PipeDataNodePluginAgent.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/plugin/PipeDataNodePluginAgent.java
@@ -37,6 +37,7 @@ import java.io.IOException;
 import java.lang.reflect.InvocationTargetException;
 import java.nio.ByteBuffer;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.locks.ReentrantLock;
 
 public class PipeDataNodePluginAgent {
@@ -215,4 +216,18 @@ public class PipeDataNodePluginAgent {
     schemaRegionAgent.validate(
         pipeName, extractorAttributes, processorAttributes, 
connectorAttributes);
   }
+
+  public boolean checkIfPluginSameType(final String oldPluginName, final 
String newPluginName) {
+    PipePluginMeta oldPipePluginMeta = 
pipePluginMetaKeeper.getPipePluginMeta(oldPluginName);
+    PipePluginMeta newPipePluginMeta = 
pipePluginMetaKeeper.getPipePluginMeta(newPluginName);
+
+    if (oldPipePluginMeta == null) {
+      throw new PipeException(String.format("plugin %s is not registered.", 
oldPluginName));
+    }
+    if (newPipePluginMeta == null) {
+      throw new PipeException(String.format("plugin %s is not registered.", 
newPluginName));
+    }
+
+    return Objects.equals(oldPipePluginMeta.getClassName(), 
(newPipePluginMeta.getClassName()));
+  }
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/plugin/dataregion/PipeDataRegionExtractorConstructor.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/plugin/dataregion/PipeDataRegionExtractorConstructor.java
index ddf6cdb6b8b..2fbc9935a25 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/plugin/dataregion/PipeDataRegionExtractorConstructor.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/plugin/dataregion/PipeDataRegionExtractorConstructor.java
@@ -24,6 +24,7 @@ import 
org.apache.iotdb.commons.pipe.agent.plugin.builtin.extractor.donothing.Do
 import 
org.apache.iotdb.commons.pipe.agent.plugin.constructor.PipeExtractorConstructor;
 import 
org.apache.iotdb.commons.pipe.agent.plugin.meta.DataNodePipePluginMetaKeeper;
 import org.apache.iotdb.db.pipe.extractor.dataregion.IoTDBDataRegionExtractor;
+import org.apache.iotdb.db.pipe.extractor.mqtt.MQTTExtractor;
 
 class PipeDataRegionExtractorConstructor extends PipeExtractorConstructor {
 
@@ -42,5 +43,9 @@ class PipeDataRegionExtractorConstructor extends 
PipeExtractorConstructor {
         BuiltinPipePlugin.DO_NOTHING_SOURCE.getPipePluginName(), 
DoNothingExtractor::new);
     pluginConstructors.put(
         BuiltinPipePlugin.IOTDB_SOURCE.getPipePluginName(), 
IoTDBDataRegionExtractor::new);
+
+    pluginConstructors.put(
+        BuiltinPipePlugin.MQTT_EXTRACTOR.getPipePluginName(), 
MQTTExtractor::new);
+    pluginConstructors.put(BuiltinPipePlugin.MQTT_SOURCE.getPipePluginName(), 
MQTTExtractor::new);
   }
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/PipeDataNodeTaskAgent.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/PipeDataNodeTaskAgent.java
index 9f657446f74..f09422fa663 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/PipeDataNodeTaskAgent.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/PipeDataNodeTaskAgent.java
@@ -27,6 +27,7 @@ import 
org.apache.iotdb.commons.exception.IllegalPathException;
 import org.apache.iotdb.commons.pipe.agent.task.PipeTask;
 import org.apache.iotdb.commons.pipe.agent.task.PipeTaskAgent;
 import org.apache.iotdb.commons.pipe.agent.task.meta.PipeMeta;
+import org.apache.iotdb.commons.pipe.agent.task.meta.PipeRuntimeMeta;
 import org.apache.iotdb.commons.pipe.agent.task.meta.PipeStaticMeta;
 import org.apache.iotdb.commons.pipe.agent.task.meta.PipeStatus;
 import org.apache.iotdb.commons.pipe.agent.task.meta.PipeTaskMeta;
@@ -120,7 +121,9 @@ public class PipeDataNodeTaskAgent extends PipeTaskAgent {
   @Override
   protected Map<Integer, PipeTask> buildPipeTasks(final PipeMeta 
pipeMetaFromConfigNode)
       throws IllegalPathException {
-    return new PipeDataNodeBuilder(pipeMetaFromConfigNode).build();
+    return pipeMetaFromConfigNode.getStaticMeta().isSourceExternal()
+        ? new 
PipeDataNodeBuilder(pipeMetaFromConfigNode).buildTasksWithExternalSource()
+        : new 
PipeDataNodeBuilder(pipeMetaFromConfigNode).buildTasksWithInternalSource();
   }
 
   ////////////////////////// Manage by Pipe Name //////////////////////////
@@ -194,7 +197,12 @@ public class PipeDataNodeTaskAgent extends PipeTaskAgent {
                   consensusGroupId, extractorParameters);
 
       // Advance the extractor parameters parsing logic to avoid creating 
un-relevant pipeTasks
-      if (needConstructDataRegionTask || needConstructSchemaRegionTask) {
+      if (
+      // For external source
+      PipeRuntimeMeta.isSourceExternal(consensusGroupId)
+          // For internal source
+          || needConstructDataRegionTask
+          || needConstructSchemaRegionTask) {
         final PipeDataNodeTask pipeTask =
             new PipeDataNodeTaskBuilder(pipeStaticMeta, consensusGroupId, 
pipeTaskMeta).build();
         pipeTask.create();
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/builder/PipeDataNodeBuilder.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/builder/PipeDataNodeBuilder.java
index 5ed04fd2d17..af07ef716e2 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/builder/PipeDataNodeBuilder.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/builder/PipeDataNodeBuilder.java
@@ -49,7 +49,7 @@ public class PipeDataNodeBuilder {
     this.pipeMeta = pipeMeta;
   }
 
-  public Map<Integer, PipeTask> build() throws IllegalPathException {
+  public Map<Integer, PipeTask> buildTasksWithInternalSource() throws 
IllegalPathException {
     final PipeStaticMeta pipeStaticMeta = pipeMeta.getStaticMeta();
     final PipeRuntimeMeta pipeRuntimeMeta = pipeMeta.getRuntimeMeta();
 
@@ -84,4 +84,22 @@ public class PipeDataNodeBuilder {
     }
     return consensusGroupIdToPipeTaskMap;
   }
+
+  public Map<Integer, PipeTask> buildTasksWithExternalSource() {
+    final Map<Integer, PipeTask> taskIdToPipeTaskMap = new HashMap<>();
+    final PipeStaticMeta pipeStaticMeta = pipeMeta.getStaticMeta();
+    final PipeRuntimeMeta pipeRuntimeMeta = pipeMeta.getRuntimeMeta();
+
+    for (Map.Entry<Integer, PipeTaskMeta> taskIdToPipeTaskMeta :
+        pipeRuntimeMeta.getConsensusGroupId2TaskMetaMap().entrySet()) {
+      final int taskId = taskIdToPipeTaskMeta.getKey();
+      final PipeTaskMeta pipeTaskMeta = taskIdToPipeTaskMeta.getValue();
+      if (pipeTaskMeta.getLeaderNodeId() == CONFIG.getDataNodeId()) {
+        taskIdToPipeTaskMap.put(
+            taskId, new PipeDataNodeTaskBuilder(pipeStaticMeta, taskId, 
pipeTaskMeta).build());
+      }
+    }
+
+    return taskIdToPipeTaskMap;
+  }
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/builder/PipeDataNodeTaskBuilder.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/builder/PipeDataNodeTaskBuilder.java
index 5be9dc8ab64..6bcea653d64 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/builder/PipeDataNodeTaskBuilder.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/builder/PipeDataNodeTaskBuilder.java
@@ -21,10 +21,12 @@ package org.apache.iotdb.db.pipe.agent.task.builder;
 
 import org.apache.iotdb.commons.consensus.index.impl.MinimumProgressIndex;
 import org.apache.iotdb.commons.exception.IllegalPathException;
+import org.apache.iotdb.commons.pipe.agent.plugin.builtin.BuiltinPipePlugin;
 import org.apache.iotdb.commons.pipe.agent.task.meta.PipeStaticMeta;
 import org.apache.iotdb.commons.pipe.agent.task.meta.PipeTaskMeta;
 import org.apache.iotdb.commons.pipe.agent.task.meta.PipeType;
 import org.apache.iotdb.commons.pipe.config.constant.PipeConnectorConstant;
+import org.apache.iotdb.commons.pipe.config.constant.PipeExtractorConstant;
 import org.apache.iotdb.commons.pipe.config.constant.SystemConstant;
 import org.apache.iotdb.db.pipe.agent.task.PipeDataNodeTask;
 import 
org.apache.iotdb.db.pipe.agent.task.execution.PipeConnectorSubtaskExecutor;
@@ -101,6 +103,7 @@ public class PipeDataNodeTaskBuilder {
     final PipeParameters connectorParameters =
         blendUserAndSystemParameters(pipeStaticMeta.getConnectorParameters());
     checkConflict(extractorParameters, connectorParameters);
+    injectParameters(extractorParameters, connectorParameters);
 
     // We first build the extractor and connector, then build the processor.
     final PipeTaskExtractorStage extractorStage =
@@ -239,4 +242,31 @@ public class PipeDataNodeTaskBuilder {
       }
     }
   }
+
+  private void injectParameters(
+      final PipeParameters extractorParameters, final PipeParameters 
connectorParameters) {
+    final boolean isSourceExternal =
+        !BuiltinPipePlugin.BUILTIN_SOURCES.contains(
+            extractorParameters
+                .getStringOrDefault(
+                    Arrays.asList(
+                        PipeExtractorConstant.EXTRACTOR_KEY, 
PipeExtractorConstant.SOURCE_KEY),
+                    BuiltinPipePlugin.IOTDB_EXTRACTOR.getPipePluginName())
+                .toLowerCase());
+
+    final String connectorPluginName =
+        connectorParameters
+            .getStringOrDefault(
+                Arrays.asList(PipeConnectorConstant.CONNECTOR_KEY, 
PipeConnectorConstant.SINK_KEY),
+                BuiltinPipePlugin.IOTDB_THRIFT_SINK.getPipePluginName())
+            .toLowerCase();
+    final boolean isWriteBackSink =
+        
BuiltinPipePlugin.WRITE_BACK_CONNECTOR.getPipePluginName().equals(connectorPluginName)
+            || 
BuiltinPipePlugin.WRITE_BACK_SINK.getPipePluginName().equals(connectorPluginName);
+
+    if (isSourceExternal && isWriteBackSink) {
+      connectorParameters.addAttribute(
+          PipeConnectorConstant.CONNECTOR_USE_EVENT_USER_NAME_KEY, 
Boolean.TRUE.toString());
+    }
+  }
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/stage/PipeTaskExtractorStage.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/stage/PipeTaskExtractorStage.java
index f6a2030e5fa..df1a7079f63 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/stage/PipeTaskExtractorStage.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/stage/PipeTaskExtractorStage.java
@@ -21,6 +21,7 @@ package org.apache.iotdb.db.pipe.agent.task.stage;
 
 import org.apache.iotdb.commons.consensus.DataRegionId;
 import org.apache.iotdb.commons.pipe.agent.task.connection.EventSupplier;
+import org.apache.iotdb.commons.pipe.agent.task.meta.PipeRuntimeMeta;
 import org.apache.iotdb.commons.pipe.agent.task.meta.PipeTaskMeta;
 import org.apache.iotdb.commons.pipe.agent.task.stage.PipeTaskStage;
 import 
org.apache.iotdb.commons.pipe.config.plugin.configuraion.PipeTaskRuntimeConfiguration;
@@ -49,6 +50,7 @@ public class PipeTaskExtractorStage extends PipeTaskStage {
       PipeTaskMeta pipeTaskMeta) {
     pipeExtractor =
         StorageEngine.getInstance().getAllDataRegionIds().contains(new 
DataRegionId(regionId))
+                || PipeRuntimeMeta.isSourceExternal(regionId)
             ? 
PipeDataNodeAgent.plugin().dataRegion().reflectExtractor(extractorParameters)
             : 
PipeDataNodeAgent.plugin().schemaRegion().reflectExtractor(extractorParameters);
 
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/stage/PipeTaskProcessorStage.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/stage/PipeTaskProcessorStage.java
index 3be40341f03..2e887f74bf0 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/stage/PipeTaskProcessorStage.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/stage/PipeTaskProcessorStage.java
@@ -23,6 +23,7 @@ import org.apache.iotdb.commons.consensus.DataRegionId;
 import org.apache.iotdb.commons.pipe.agent.plugin.builtin.BuiltinPipePlugin;
 import org.apache.iotdb.commons.pipe.agent.task.connection.EventSupplier;
 import 
org.apache.iotdb.commons.pipe.agent.task.connection.UnboundedBlockingPendingQueue;
+import org.apache.iotdb.commons.pipe.agent.task.meta.PipeRuntimeMeta;
 import org.apache.iotdb.commons.pipe.agent.task.meta.PipeStaticMeta;
 import org.apache.iotdb.commons.pipe.agent.task.meta.PipeTaskMeta;
 import org.apache.iotdb.commons.pipe.agent.task.stage.PipeTaskStage;
@@ -77,6 +78,7 @@ public class PipeTaskProcessorStage extends PipeTaskStage {
                 pipeName, creationTime, regionId, pipeTaskMeta));
     final PipeProcessor pipeProcessor =
         StorageEngine.getInstance().getAllDataRegionIds().contains(new 
DataRegionId(regionId))
+                || PipeRuntimeMeta.isSourceExternal(regionId)
             ? PipeDataNodeAgent.plugin()
                 .dataRegion()
                 .getConfiguredProcessor(
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/connector/PipeConnectorSubtaskManager.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/connector/PipeConnectorSubtaskManager.java
index 10dc41a9323..a79ff6e4b58 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/connector/PipeConnectorSubtaskManager.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/connector/PipeConnectorSubtaskManager.java
@@ -22,6 +22,7 @@ package org.apache.iotdb.db.pipe.agent.task.subtask.connector;
 import org.apache.iotdb.commons.consensus.DataRegionId;
 import org.apache.iotdb.commons.pipe.agent.plugin.builtin.BuiltinPipePlugin;
 import 
org.apache.iotdb.commons.pipe.agent.task.connection.UnboundedBlockingPendingQueue;
+import org.apache.iotdb.commons.pipe.agent.task.meta.PipeRuntimeMeta;
 import 
org.apache.iotdb.commons.pipe.agent.task.progress.PipeEventCommitManager;
 import org.apache.iotdb.commons.pipe.config.constant.PipeConnectorConstant;
 import org.apache.iotdb.commons.pipe.config.constant.SystemConstant;
@@ -78,8 +79,9 @@ public class PipeConnectorSubtaskManager {
 
     final boolean isDataRegionConnector =
         StorageEngine.getInstance()
-            .getAllDataRegionIds()
-            .contains(new DataRegionId(environment.getRegionId()));
+                .getAllDataRegionIds()
+                .contains(new DataRegionId(environment.getRegionId()))
+            || PipeRuntimeMeta.isSourceExternal(environment.getRegionId());
 
     final int connectorNum;
     boolean realTimeFirst = false;
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtask.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtask.java
index 40352766630..4b11ef97285 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtask.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/task/subtask/processor/PipeProcessorSubtask.java
@@ -24,6 +24,7 @@ import 
org.apache.iotdb.commons.exception.pipe.PipeRuntimeException;
 import 
org.apache.iotdb.commons.exception.pipe.PipeRuntimeOutOfMemoryCriticalException;
 import org.apache.iotdb.commons.pipe.agent.task.connection.EventSupplier;
 import org.apache.iotdb.commons.pipe.agent.task.execution.PipeSubtaskScheduler;
+import org.apache.iotdb.commons.pipe.agent.task.meta.PipeRuntimeMeta;
 import 
org.apache.iotdb.commons.pipe.agent.task.progress.PipeEventCommitManager;
 import org.apache.iotdb.commons.pipe.agent.task.subtask.PipeReportableSubtask;
 import org.apache.iotdb.commons.pipe.event.EnrichedEvent;
@@ -89,7 +90,8 @@ public class PipeProcessorSubtask extends 
PipeReportableSubtask {
     this.subtaskCreationTime = System.currentTimeMillis();
 
     // Only register dataRegions
-    if (StorageEngine.getInstance().getAllDataRegionIds().contains(new 
DataRegionId(regionId))) {
+    if (StorageEngine.getInstance().getAllDataRegionIds().contains(new 
DataRegionId(regionId))
+        || PipeRuntimeMeta.isSourceExternal(regionId)) {
       PipeProcessorMetrics.getInstance().register(this);
     }
   }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/connector/protocol/writeback/WriteBackConnector.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/connector/protocol/writeback/WriteBackConnector.java
index 0bc4f76d25c..37515279ead 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/connector/protocol/writeback/WriteBackConnector.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/connector/protocol/writeback/WriteBackConnector.java
@@ -29,6 +29,7 @@ import org.apache.iotdb.db.conf.IoTDBDescriptor;
 import 
org.apache.iotdb.db.pipe.connector.payload.evolvable.request.PipeTransferTabletBinaryReqV2;
 import 
org.apache.iotdb.db.pipe.connector.payload.evolvable.request.PipeTransferTabletInsertNodeReqV2;
 import 
org.apache.iotdb.db.pipe.connector.payload.evolvable.request.PipeTransferTabletRawReqV2;
+import 
org.apache.iotdb.db.pipe.event.common.statement.PipeStatementInsertionEvent;
 import 
org.apache.iotdb.db.pipe.event.common.tablet.PipeInsertNodeTabletInsertionEvent;
 import 
org.apache.iotdb.db.pipe.event.common.tablet.PipeRawTabletInsertionEvent;
 import org.apache.iotdb.db.protocol.session.IClientSession;
@@ -79,9 +80,12 @@ import static 
org.apache.iotdb.commons.pipe.config.constant.PipeConnectorConstan
 import static 
org.apache.iotdb.commons.pipe.config.constant.PipeConnectorConstant.CONNECTOR_IOTDB_USERNAME_KEY;
 import static 
org.apache.iotdb.commons.pipe.config.constant.PipeConnectorConstant.CONNECTOR_IOTDB_USER_KEY;
 import static 
org.apache.iotdb.commons.pipe.config.constant.PipeConnectorConstant.CONNECTOR_SKIP_IF_KEY;
+import static 
org.apache.iotdb.commons.pipe.config.constant.PipeConnectorConstant.CONNECTOR_USE_EVENT_USER_NAME_DEFAULT_VALUE;
+import static 
org.apache.iotdb.commons.pipe.config.constant.PipeConnectorConstant.CONNECTOR_USE_EVENT_USER_NAME_KEY;
 import static 
org.apache.iotdb.commons.pipe.config.constant.PipeConnectorConstant.SINK_IOTDB_USERNAME_KEY;
 import static 
org.apache.iotdb.commons.pipe.config.constant.PipeConnectorConstant.SINK_IOTDB_USER_KEY;
 import static 
org.apache.iotdb.commons.pipe.config.constant.PipeConnectorConstant.SINK_SKIP_IF_KEY;
+import static 
org.apache.iotdb.commons.pipe.config.constant.PipeConnectorConstant.SINK_USE_EVENT_USER_NAME_KEY;
 import static 
org.apache.iotdb.db.exception.metadata.DatabaseNotSetException.DATABASE_NOT_SET;
 import static org.apache.iotdb.db.utils.ErrorHandlingUtils.getRootCause;
 
@@ -100,6 +104,7 @@ public class WriteBackConnector implements PipeConnector {
   // Temporary, used to separate
   private IClientSession treeSession;
   private boolean skipIfNoPrivileges;
+  private boolean useEventUserName;
 
   private static final String TREE_MODEL_DATABASE_NAME_IDENTIFIER = null;
 
@@ -168,6 +173,11 @@ public class WriteBackConnector implements PipeConnector {
       throw new PipeParameterNotValidException(
           String.format("Parameters in set %s are not allowed in 'skipif'", 
skipIfOptionSet));
     }
+
+    useEventUserName =
+        parameters.getBooleanOrDefault(
+            Arrays.asList(CONNECTOR_USE_EVENT_USER_NAME_KEY, 
SINK_USE_EVENT_USER_NAME_KEY),
+            CONNECTOR_USE_EVENT_USER_NAME_DEFAULT_VALUE);
   }
 
   @Override
@@ -240,8 +250,10 @@ public class WriteBackConnector implements PipeConnector {
 
     final TSStatus status =
         insertBaseStatement.isWriteToTable()
-            ? executeStatementForTableModel(insertBaseStatement, dataBaseName)
-            : executeStatementForTreeModel(insertBaseStatement);
+            ? executeStatementForTableModel(
+                insertBaseStatement, dataBaseName, 
pipeInsertNodeTabletInsertionEvent.getUserName())
+            : executeStatementForTreeModel(
+                insertBaseStatement, 
pipeInsertNodeTabletInsertionEvent.getUserName());
 
     if (status.getCode() != TSStatusCode.REDIRECTION_RECOMMEND.getStatusCode()
         && status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
@@ -281,9 +293,10 @@ public class WriteBackConnector implements PipeConnector {
 
     final TSStatus status =
         insertTabletStatement.isWriteToTable()
-            ? executeStatementForTableModel(insertTabletStatement, 
dataBaseName)
-            : executeStatementForTreeModel(insertTabletStatement);
-
+            ? executeStatementForTableModel(
+                insertTabletStatement, dataBaseName, 
pipeRawTabletInsertionEvent.getUserName())
+            : executeStatementForTreeModel(
+                insertTabletStatement, 
pipeRawTabletInsertionEvent.getUserName());
     if (status.getCode() != TSStatusCode.REDIRECTION_RECOMMEND.getStatusCode()
         && status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()
         && !(skipIfNoPrivileges
@@ -297,7 +310,45 @@ public class WriteBackConnector implements PipeConnector {
 
   @Override
   public void transfer(final Event event) throws Exception {
-    // Ignore the event except TabletInsertionEvent
+    // only transfer PipeStatementInsertionEvent
+    if (event instanceof PipeStatementInsertionEvent) {
+      doTransferWrapper((PipeStatementInsertionEvent) event);
+    }
+  }
+
+  private void doTransferWrapper(final PipeStatementInsertionEvent 
pipeStatementInsertionEvent)
+      throws PipeException {
+    // We increase the reference count for this event to determine if the 
event may be released.
+    if 
(!pipeStatementInsertionEvent.increaseReferenceCount(WriteBackConnector.class.getName()))
 {
+      return;
+    }
+    try {
+      doTransfer(pipeStatementInsertionEvent);
+    } finally {
+      
pipeStatementInsertionEvent.decreaseReferenceCount(WriteBackConnector.class.getName(),
 false);
+    }
+  }
+
+  private void doTransfer(final PipeStatementInsertionEvent 
pipeStatementInsertionEvent)
+      throws PipeException {
+
+    final TSStatus status =
+        pipeStatementInsertionEvent.isTableModelEvent()
+            ? executeStatementForTableModel(
+                pipeStatementInsertionEvent.getStatement(),
+                pipeStatementInsertionEvent.getTableModelDatabaseName(),
+                pipeStatementInsertionEvent.getUserName())
+            : executeStatementForTreeModel(
+                pipeStatementInsertionEvent.getStatement(),
+                pipeStatementInsertionEvent.getUserName());
+
+    if (status.getCode() != TSStatusCode.REDIRECTION_RECOMMEND.getStatusCode()
+        && status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+      throw new PipeException(
+          String.format(
+              "Write back PipeStatementInsertionEvent %s error, result status 
%s",
+              pipeStatementInsertionEvent, status));
+    }
   }
 
   @Override
@@ -310,9 +361,14 @@ public class WriteBackConnector implements PipeConnector {
     }
   }
 
-  private TSStatus executeStatementForTableModel(Statement statement, String 
dataBaseName) {
+  private TSStatus executeStatementForTableModel(
+      Statement statement, String dataBaseName, final String userName) {
     session.setDatabaseName(dataBaseName);
     session.setSqlDialect(IClientSession.SqlDialect.TABLE);
+    final String originalUserName = session.getUsername();
+    if (useEventUserName && userName != null) {
+      session.setUsername(userName);
+    }
     SESSION_MANAGER.registerSession(session);
     try {
       autoCreateDatabaseIfNecessary(dataBaseName);
@@ -366,6 +422,9 @@ public class WriteBackConnector implements PipeConnector {
       throw e;
     } finally {
       SESSION_MANAGER.removeCurrSession();
+      if (useEventUserName) {
+        session.setUsername(originalUserName);
+      }
     }
   }
 
@@ -410,9 +469,13 @@ public class WriteBackConnector implements PipeConnector {
     ALREADY_CREATED_DATABASES.add(database);
   }
 
-  private TSStatus executeStatementForTreeModel(final Statement statement) {
+  private TSStatus executeStatementForTreeModel(final Statement statement, 
final String userName) {
     treeSession.setDatabaseName(null);
     treeSession.setSqlDialect(IClientSession.SqlDialect.TREE);
+    final String originalUserName = treeSession.getUsername();
+    if (useEventUserName && userName != null) {
+      treeSession.setUsername(userName);
+    }
     SESSION_MANAGER.registerSession(treeSession);
     try {
       return Coordinator.getInstance()
@@ -428,6 +491,9 @@ public class WriteBackConnector implements PipeConnector {
           .status;
     } finally {
       SESSION_MANAGER.removeCurrSession();
+      if (useEventUserName) {
+        treeSession.setUsername(originalUserName);
+      }
     }
   }
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/statement/PipeStatementInsertionEvent.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/statement/PipeStatementInsertionEvent.java
new file mode 100644
index 00000000000..1efe3aa2ac7
--- /dev/null
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/statement/PipeStatementInsertionEvent.java
@@ -0,0 +1,222 @@
+/*
+ * 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.iotdb.db.pipe.event.common.statement;
+
+import org.apache.iotdb.commons.consensus.index.ProgressIndex;
+import org.apache.iotdb.commons.consensus.index.impl.MinimumProgressIndex;
+import org.apache.iotdb.commons.pipe.agent.task.meta.PipeTaskMeta;
+import org.apache.iotdb.commons.pipe.datastructure.pattern.TablePattern;
+import org.apache.iotdb.commons.pipe.datastructure.pattern.TreePattern;
+import org.apache.iotdb.commons.pipe.event.EnrichedEvent;
+import org.apache.iotdb.commons.pipe.resource.ref.PipePhantomReferenceManager;
+import org.apache.iotdb.db.pipe.event.ReferenceTrackableEvent;
+import org.apache.iotdb.db.pipe.event.common.PipeInsertionEvent;
+import 
org.apache.iotdb.db.pipe.metric.overview.PipeDataNodeRemainingEventAndTimeMetrics;
+import org.apache.iotdb.db.pipe.resource.PipeDataNodeResourceManager;
+import org.apache.iotdb.db.pipe.resource.memory.PipeTabletMemoryBlock;
+import org.apache.iotdb.db.queryengine.plan.statement.Statement;
+import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertBaseStatement;
+
+import org.apache.tsfile.utils.RamUsageEstimator;
+
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class PipeStatementInsertionEvent extends PipeInsertionEvent
+    implements ReferenceTrackableEvent, AutoCloseable {
+
+  // For better calculation
+  private static final long INSTANCE_SIZE =
+      
RamUsageEstimator.shallowSizeOfInstance(PipeStatementInsertionEvent.class);
+  private InsertBaseStatement statement;
+
+  private volatile ProgressIndex progressIndex;
+  private boolean needToReport;
+
+  private final PipeTabletMemoryBlock allocatedMemoryBlock;
+
+  public PipeStatementInsertionEvent(
+      String pipeName,
+      long creationTime,
+      PipeTaskMeta pipeTaskMeta,
+      TreePattern treePattern,
+      TablePattern tablePattern,
+      String userName,
+      boolean skipIfNoPrivileges,
+      Boolean isTableModelEvent,
+      String databaseNameFromDataRegion,
+      InsertBaseStatement statement) {
+    super(
+        pipeName,
+        creationTime,
+        pipeTaskMeta,
+        treePattern,
+        tablePattern,
+        userName,
+        skipIfNoPrivileges,
+        Long.MIN_VALUE,
+        Long.MAX_VALUE,
+        isTableModelEvent,
+        databaseNameFromDataRegion,
+        null,
+        null);
+    this.statement = statement;
+    // Allocate empty memory block, will be resized later.
+    this.allocatedMemoryBlock =
+        
PipeDataNodeResourceManager.memory().forceAllocateForTabletWithRetry(0);
+  }
+
+  @Override
+  public boolean internallyIncreaseResourceReferenceCount(String 
holderMessage) {
+    PipeDataNodeResourceManager.memory()
+        .forceResize(allocatedMemoryBlock, statement.ramBytesUsed() + 
INSTANCE_SIZE);
+    if (Objects.nonNull(pipeName)) {
+      PipeDataNodeRemainingEventAndTimeMetrics.getInstance()
+          .increaseTabletEventCount(pipeName, creationTime);
+    }
+    return true;
+  }
+
+  @Override
+  public boolean internallyDecreaseResourceReferenceCount(String 
holderMessage) {
+    if (Objects.nonNull(pipeName)) {
+      PipeDataNodeRemainingEventAndTimeMetrics.getInstance()
+          .decreaseTabletEventCount(pipeName, creationTime);
+    }
+    allocatedMemoryBlock.close();
+
+    statement = null;
+    return true;
+  }
+
+  @Override
+  public void bindProgressIndex(final ProgressIndex progressIndex) {
+    // Normally not all events need to report progress, but if the 
progressIndex
+    // is given, indicating that the progress needs to be reported.
+    if (Objects.nonNull(progressIndex)) {
+      markAsNeedToReport();
+    }
+
+    this.progressIndex = progressIndex;
+  }
+
+  @Override
+  public ProgressIndex getProgressIndex() {
+    return progressIndex == null ? MinimumProgressIndex.INSTANCE : 
progressIndex;
+  }
+
+  @Override
+  public EnrichedEvent shallowCopySelfAndBindPipeTaskMetaForProgressReport(
+      String pipeName,
+      long creationTime,
+      PipeTaskMeta pipeTaskMeta,
+      TreePattern treePattern,
+      TablePattern tablePattern,
+      String userName,
+      boolean skipIfNoPrivileges,
+      long startTime,
+      long endTime) {
+    throw new UnsupportedOperationException(
+        "shallowCopySelfAndBindPipeTaskMetaForProgressReport() is not 
supported!");
+  }
+
+  @Override
+  public boolean isGeneratedByPipe() {
+    return false;
+  }
+
+  @Override
+  public boolean mayEventTimeOverlappedWithTimeRange() {
+    throw new UnsupportedOperationException(
+        "mayEventTimeOverlappedWithTimeRange() is not supported!");
+  }
+
+  @Override
+  public boolean mayEventPathsOverlappedWithPattern() {
+    throw new UnsupportedOperationException(
+        "mayEventPathsOverlappedWithPattern() is not supported!");
+  }
+
+  public void markAsNeedToReport() {
+    this.needToReport = true;
+  }
+
+  public Statement getStatement() {
+    return statement;
+  }
+
+  /////////////////////////// Object ///////////////////////////
+
+  @Override
+  public String toString() {
+    return String.format(
+            "PipeStatementInsertionEvent{statement=%s, needToReport=%s, 
allocatedMemoryBlock=%s}",
+            statement, needToReport, allocatedMemoryBlock)
+        + " - "
+        + super.toString();
+  }
+
+  @Override
+  public String coreReportMessage() {
+    return String.format(
+            "PipeStatementInsertionEvent{statement=%s, needToReport=%s, 
allocatedMemoryBlock=%s}",
+            statement, needToReport, allocatedMemoryBlock)
+        + " - "
+        + super.coreReportMessage();
+  }
+
+  /////////////////////////// ReferenceTrackableEvent 
//////////////////////////////
+
+  @Override
+  protected void trackResource() {
+    PipeDataNodeResourceManager.ref().trackPipeEventResource(this, 
eventResourceBuilder());
+  }
+
+  @Override
+  public PipePhantomReferenceManager.PipeEventResource eventResourceBuilder() {
+    return new PipeStatementInsertionEventResource(
+        this.isReleased, this.referenceCount, this.allocatedMemoryBlock);
+  }
+
+  private static class PipeStatementInsertionEventResource
+      extends PipePhantomReferenceManager.PipeEventResource {
+
+    private final PipeTabletMemoryBlock allocatedMemoryBlock;
+
+    private PipeStatementInsertionEventResource(
+        final AtomicBoolean isReleased,
+        final AtomicInteger referenceCount,
+        final PipeTabletMemoryBlock allocatedMemoryBlock) {
+      super(isReleased, referenceCount);
+      this.allocatedMemoryBlock = allocatedMemoryBlock;
+    }
+
+    @Override
+    protected void finalizeResource() {
+      allocatedMemoryBlock.close();
+    }
+  }
+
+  /////////////////////////// AutoCloseable ///////////////////////////
+
+  @Override
+  public void close() throws Exception {}
+}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tablet/PipeRawTabletInsertionEvent.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tablet/PipeRawTabletInsertionEvent.java
index 82e36f53735..b2a6fc74922 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tablet/PipeRawTabletInsertionEvent.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tablet/PipeRawTabletInsertionEvent.java
@@ -142,6 +142,39 @@ public class PipeRawTabletInsertionEvent extends 
PipeInsertionEvent
         Long.MAX_VALUE);
   }
 
+  public PipeRawTabletInsertionEvent(
+      final Boolean isTableModelEvent,
+      final String databaseName,
+      final String tableModelDataBaseName,
+      final String treeModelDataBaseName,
+      final Tablet tablet,
+      final boolean isAligned,
+      final String pipeName,
+      final long creationTime,
+      final PipeTaskMeta pipeTaskMeta,
+      final EnrichedEvent sourceEvent,
+      final boolean needToReport,
+      final String userName) {
+    this(
+        isTableModelEvent,
+        databaseName,
+        tableModelDataBaseName,
+        treeModelDataBaseName,
+        tablet,
+        isAligned,
+        sourceEvent,
+        needToReport,
+        pipeName,
+        creationTime,
+        pipeTaskMeta,
+        null,
+        null,
+        userName,
+        true,
+        Long.MIN_VALUE,
+        Long.MAX_VALUE);
+  }
+
   @TestOnly
   public PipeRawTabletInsertionEvent(final Tablet tablet, final boolean 
isAligned) {
     this(
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/extractor/mqtt/MQTTExtractor.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/extractor/mqtt/MQTTExtractor.java
new file mode 100644
index 00000000000..e032747673e
--- /dev/null
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/extractor/mqtt/MQTTExtractor.java
@@ -0,0 +1,182 @@
+/*
+ * 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.iotdb.db.pipe.extractor.mqtt;
+
+import 
org.apache.iotdb.commons.pipe.agent.task.connection.UnboundedBlockingPendingQueue;
+import org.apache.iotdb.commons.pipe.agent.task.meta.PipeTaskMeta;
+import org.apache.iotdb.commons.pipe.config.constant.PipeExtractorConstant;
+import 
org.apache.iotdb.commons.pipe.config.plugin.env.PipeTaskExtractorRuntimeEnvironment;
+import org.apache.iotdb.commons.pipe.event.EnrichedEvent;
+import org.apache.iotdb.db.pipe.metric.source.PipeDataRegionEventCounter;
+import org.apache.iotdb.db.protocol.mqtt.BrokerAuthenticator;
+import org.apache.iotdb.pipe.api.PipeExtractor;
+import org.apache.iotdb.pipe.api.annotation.TableModel;
+import org.apache.iotdb.pipe.api.annotation.TreeModel;
+import 
org.apache.iotdb.pipe.api.customizer.configuration.PipeExtractorRuntimeConfiguration;
+import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameterValidator;
+import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameters;
+import org.apache.iotdb.pipe.api.event.Event;
+import org.apache.iotdb.pipe.api.exception.PipeParameterNotValidException;
+
+import io.moquette.BrokerConstants;
+import io.moquette.broker.Server;
+import io.moquette.broker.config.IConfig;
+import io.moquette.broker.config.MemoryConfig;
+import io.moquette.broker.security.IAuthenticator;
+import io.moquette.interception.InterceptHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * MQTTExtractor is an external Extractor that uses the MQTT protocol to 
receive data. It starts an
+ * MQTT broker and listens for incoming messages, which are then processed and 
passed to the pending
+ * queue.
+ */
+@TreeModel
+@TableModel
+public class MQTTExtractor implements PipeExtractor {
+
+  private static final Logger LOGGER = 
LoggerFactory.getLogger(MQTTExtractor.class);
+
+  protected String pipeName;
+  protected long creationTime;
+  protected PipeTaskMeta pipeTaskMeta;
+  protected final UnboundedBlockingPendingQueue<EnrichedEvent> pendingQueue =
+      new UnboundedBlockingPendingQueue<>(new PipeDataRegionEventCounter());
+
+  protected IConfig config;
+  protected List<InterceptHandler> handlers;
+  protected IAuthenticator authenticator;
+  private final Server server = new Server();
+
+  protected final AtomicBoolean isClosed = new AtomicBoolean(false);
+
+  @Override
+  public void validate(final PipeParameterValidator validator) throws 
Exception {
+    if (!validator
+        .getParameters()
+        .getBooleanOrDefault(
+            Arrays.asList(
+                
PipeExtractorConstant.EXTERNAL_EXTRACTOR_SINGLE_INSTANCE_PER_NODE_KEY,
+                
PipeExtractorConstant.EXTERNAL_SOURCE_SINGLE_INSTANCE_PER_NODE_KEY),
+            
PipeExtractorConstant.EXTERNAL_EXTRACTOR_SINGLE_INSTANCE_PER_NODE_DEFAULT_VALUE))
 {
+      throw new PipeParameterNotValidException("single mode should be true in 
MQTT extractor");
+    }
+  }
+
+  @Override
+  public void customize(
+      final PipeParameters parameters, final PipeExtractorRuntimeConfiguration 
configuration)
+      throws Exception {
+    final PipeTaskExtractorRuntimeEnvironment environment =
+        (PipeTaskExtractorRuntimeEnvironment) 
configuration.getRuntimeEnvironment();
+    pipeName = environment.getPipeName();
+    creationTime = environment.getCreationTime();
+    pipeTaskMeta = environment.getPipeTaskMeta();
+    config = createBrokerConfig(parameters);
+    handlers = new ArrayList<>(1);
+    handlers.add(new MQTTPublishHandler(parameters, environment, 
pendingQueue));
+    authenticator = new BrokerAuthenticator();
+  }
+
+  private IConfig createBrokerConfig(final PipeParameters pipeParameters) {
+    final Properties properties = new Properties();
+    properties.setProperty(
+        BrokerConstants.HOST_PROPERTY_NAME,
+        pipeParameters.getStringOrDefault(
+            PipeExtractorConstant.MQTT_BROKER_HOST_KEY,
+            PipeExtractorConstant.MQTT_BROKER_HOST_DEFAULT_VALUE));
+    properties.setProperty(
+        BrokerConstants.PORT_PROPERTY_NAME,
+        pipeParameters.getStringOrDefault(
+            PipeExtractorConstant.MQTT_BROKER_PORT_KEY,
+            PipeExtractorConstant.MQTT_BROKER_PORT_DEFAULT_VALUE));
+    properties.setProperty(
+        BrokerConstants.BROKER_INTERCEPTOR_THREAD_POOL_SIZE,
+        pipeParameters.getStringOrDefault(
+            PipeExtractorConstant.MQTT_BROKER_INTERCEPTOR_THREAD_POOL_SIZE_KEY,
+            String.valueOf(
+                
PipeExtractorConstant.MQTT_BROKER_INTERCEPTOR_THREAD_POOL_SIZE_DEFAULT_VALUE)));
+    properties.setProperty(
+        BrokerConstants.DATA_PATH_PROPERTY_NAME,
+        pipeParameters.getStringOrDefault(
+            PipeExtractorConstant.MQTT_DATA_PATH_PROPERTY_NAME_KEY,
+            PipeExtractorConstant.MQTT_DATA_PATH_PROPERTY_NAME_DEFAULT_VALUE));
+    properties.setProperty(
+        BrokerConstants.IMMEDIATE_BUFFER_FLUSH_PROPERTY_NAME,
+        pipeParameters.getStringOrDefault(
+            
PipeExtractorConstant.MQTT_IMMEDIATE_BUFFER_FLUSH_PROPERTY_NAME_KEY,
+            String.valueOf(
+                
PipeExtractorConstant.MQTT_IMMEDIATE_BUFFER_FLUSH_PROPERTY_NAME_DEFAULT_VALUE)));
+    properties.setProperty(
+        BrokerConstants.ALLOW_ANONYMOUS_PROPERTY_NAME,
+        pipeParameters.getStringOrDefault(
+            PipeExtractorConstant.MQTT_ALLOW_ANONYMOUS_PROPERTY_NAME_KEY,
+            String.valueOf(
+                
PipeExtractorConstant.MQTT_ALLOW_ANONYMOUS_PROPERTY_NAME_DEFAULT_VALUE)));
+    properties.setProperty(
+        BrokerConstants.ALLOW_ZERO_BYTE_CLIENT_ID_PROPERTY_NAME,
+        pipeParameters.getStringOrDefault(
+            
PipeExtractorConstant.MQTT_ALLOW_ZERO_BYTE_CLIENT_ID_PROPERTY_NAME_KEY,
+            String.valueOf(
+                
PipeExtractorConstant.MQTT_ALLOW_ZERO_BYTE_CLIENT_ID_PROPERTY_NAME_DEFAULT_VALUE)));
+    properties.setProperty(
+        BrokerConstants.NETTY_MAX_BYTES_PROPERTY_NAME,
+        pipeParameters.getStringOrDefault(
+            PipeExtractorConstant.MQTT_NETTY_MAX_BYTES_PROPERTY_NAME_KEY,
+            String.valueOf(
+                
PipeExtractorConstant.MQTT_NETTY_MAX_BYTES_PROPERTY_NAME_DEFAULT_VALUE)));
+    return new MemoryConfig(properties);
+  }
+
+  @Override
+  public void start() throws Exception {
+    try {
+      server.startServer(config, handlers, null, authenticator, null);
+    } catch (IOException e) {
+      throw new RuntimeException("Exception while starting server", e);
+    }
+
+    LOGGER.info(
+        "Start MQTT Extractor successfully, listening on ip {}, port {}",
+        config.getProperty(BrokerConstants.HOST_PROPERTY_NAME),
+        config.getProperty(BrokerConstants.PORT_PROPERTY_NAME));
+  }
+
+  @Override
+  public Event supply() throws Exception {
+    return isClosed.get() ? null : pendingQueue.directPoll();
+  }
+
+  @Override
+  public void close() throws Exception {
+    if (!isClosed.get()) {
+      server.stopServer();
+      isClosed.set(true);
+    }
+  }
+}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/extractor/mqtt/MQTTPublishHandler.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/extractor/mqtt/MQTTPublishHandler.java
new file mode 100644
index 00000000000..cb19420c402
--- /dev/null
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/extractor/mqtt/MQTTPublishHandler.java
@@ -0,0 +1,330 @@
+/*
+ * 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.iotdb.db.pipe.extractor.mqtt;
+
+import org.apache.iotdb.commons.conf.IoTDBConstant.ClientVersion;
+import org.apache.iotdb.commons.exception.IllegalPathException;
+import 
org.apache.iotdb.commons.pipe.agent.task.connection.UnboundedBlockingPendingQueue;
+import org.apache.iotdb.commons.pipe.agent.task.meta.PipeTaskMeta;
+import org.apache.iotdb.commons.pipe.config.constant.PipeExtractorConstant;
+import 
org.apache.iotdb.commons.pipe.config.plugin.env.PipeTaskExtractorRuntimeEnvironment;
+import org.apache.iotdb.commons.pipe.event.EnrichedEvent;
+import org.apache.iotdb.commons.schema.table.column.TsTableColumnCategory;
+import 
org.apache.iotdb.db.pipe.event.common.statement.PipeStatementInsertionEvent;
+import org.apache.iotdb.db.protocol.mqtt.Message;
+import org.apache.iotdb.db.protocol.mqtt.PayloadFormatManager;
+import org.apache.iotdb.db.protocol.mqtt.PayloadFormatter;
+import org.apache.iotdb.db.protocol.mqtt.TableMessage;
+import org.apache.iotdb.db.protocol.mqtt.TreeMessage;
+import org.apache.iotdb.db.protocol.session.IClientSession;
+import org.apache.iotdb.db.protocol.session.MqttClientSession;
+import org.apache.iotdb.db.protocol.session.SessionManager;
+import org.apache.iotdb.db.queryengine.plan.Coordinator;
+import 
org.apache.iotdb.db.queryengine.plan.analyze.cache.schema.DataNodeDevicePathCache;
+import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowStatement;
+import 
org.apache.iotdb.db.queryengine.plan.statement.crud.InsertTabletStatement;
+import org.apache.iotdb.db.utils.CommonUtils;
+import org.apache.iotdb.db.utils.TimestampPrecisionUtils;
+import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameters;
+import org.apache.iotdb.service.rpc.thrift.TSProtocolVersion;
+
+import io.moquette.interception.AbstractInterceptHandler;
+import io.moquette.interception.messages.InterceptConnectMessage;
+import io.moquette.interception.messages.InterceptDisconnectMessage;
+import io.moquette.interception.messages.InterceptPublishMessage;
+import io.netty.buffer.ByteBuf;
+import io.netty.handler.codec.mqtt.MqttQoS;
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.utils.BitMap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.ZoneId;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/** PublishHandler handle the messages from MQTT clients. */
+public class MQTTPublishHandler extends AbstractInterceptHandler {
+
+  private static final Logger LOGGER = 
LoggerFactory.getLogger(MQTTPublishHandler.class);
+
+  private final SessionManager sessionManager = SessionManager.getInstance();
+
+  private final ConcurrentHashMap<String, MqttClientSession> 
clientIdToSessionMap =
+      new ConcurrentHashMap<>();
+  private final PayloadFormatter payloadFormat;
+  private final boolean useTableInsert;
+  private final UnboundedBlockingPendingQueue<EnrichedEvent> pendingQueue;
+  private final String pipeName;
+  private final long creationTime;
+  private final PipeTaskMeta pipeTaskMeta;
+
+  public MQTTPublishHandler(
+      final PipeParameters pipeParameters,
+      final PipeTaskExtractorRuntimeEnvironment environment,
+      final UnboundedBlockingPendingQueue<EnrichedEvent> pendingQueue) {
+    this.payloadFormat =
+        PayloadFormatManager.getPayloadFormat(
+            pipeParameters.getStringOrDefault(
+                PipeExtractorConstant.MQTT_PAYLOAD_FORMATTER_KEY,
+                PipeExtractorConstant.MQTT_PAYLOAD_FORMATTER_DEFAULT_VALUE));
+    useTableInsert = 
PayloadFormatter.TABLE_TYPE.equals(this.payloadFormat.getType());
+    pipeName = environment.getPipeName();
+    creationTime = environment.getCreationTime();
+    pipeTaskMeta = environment.getPipeTaskMeta();
+    this.pendingQueue = pendingQueue;
+  }
+
+  @Override
+  public String getID() {
+    return "mqtt-source-broker-listener";
+  }
+
+  @Override
+  public void onConnect(InterceptConnectMessage msg) {
+    if (!clientIdToSessionMap.containsKey(msg.getClientID())) {
+      final MqttClientSession session = new 
MqttClientSession(msg.getClientID());
+      sessionManager.login(
+          session,
+          msg.getUsername(),
+          new String(msg.getPassword()),
+          ZoneId.systemDefault().toString(),
+          TSProtocolVersion.IOTDB_SERVICE_PROTOCOL_V3,
+          ClientVersion.V_1_0,
+          useTableInsert ? IClientSession.SqlDialect.TABLE : 
IClientSession.SqlDialect.TREE);
+      sessionManager.registerSession(session);
+      clientIdToSessionMap.put(msg.getClientID(), session);
+    }
+  }
+
+  @Override
+  public void onDisconnect(InterceptDisconnectMessage msg) {
+    final MqttClientSession session = 
clientIdToSessionMap.remove(msg.getClientID());
+    if (null != session) {
+      sessionManager.removeCurrSession();
+      sessionManager.closeSession(session, 
Coordinator.getInstance()::cleanupQueryExecution);
+    }
+  }
+
+  @Override
+  public void onPublish(InterceptPublishMessage msg) {
+    try {
+      final String clientId = msg.getClientID();
+      if (!clientIdToSessionMap.containsKey(clientId)) {
+        return;
+      }
+      final MqttClientSession session = 
clientIdToSessionMap.get(msg.getClientID());
+      final ByteBuf payload = msg.getPayload();
+      final String topic = msg.getTopicName();
+      final String username = msg.getUsername();
+      final MqttQoS qos = msg.getQos();
+
+      if (LOGGER.isDebugEnabled()) {
+        LOGGER.debug(
+            "Receive publish message. clientId: {}, username: {}, qos: {}, 
topic: {}, payload: {}",
+            clientId,
+            username,
+            qos,
+            topic,
+            payload);
+      }
+
+      final List<Message> messages = payloadFormat.format(payload);
+      if (messages == null) {
+        return;
+      }
+
+      for (Message message : messages) {
+        if (message == null) {
+          continue;
+        }
+        if (useTableInsert) {
+          final TableMessage tableMessage = (TableMessage) message;
+          // '/' previously defined as a database name
+          final String database =
+              !msg.getTopicName().contains("/")
+                  ? msg.getTopicName()
+                  : msg.getTopicName().substring(0, 
msg.getTopicName().indexOf("/"));
+          tableMessage.setDatabase(database.toLowerCase());
+          extractTable(tableMessage, session);
+        } else {
+          extractTree((TreeMessage) message, session);
+        }
+      }
+    } catch (Throwable t) {
+      LOGGER.warn("onPublish execution exception, msg is {}, error is ", msg, 
t);
+    } finally {
+      // release the payload of the message
+      super.onPublish(msg);
+    }
+  }
+
+  private void extractTable(final TableMessage message, final 
MqttClientSession session) {
+    try {
+      TimestampPrecisionUtils.checkTimestampPrecision(message.getTimestamp());
+      InsertTabletStatement insertTabletStatement = 
constructInsertTabletStatement(message);
+      session.setSqlDialect(IClientSession.SqlDialect.TABLE);
+      final EnrichedEvent event =
+          new PipeStatementInsertionEvent(
+              pipeName,
+              creationTime,
+              pipeTaskMeta,
+              null,
+              null,
+              session.getUsername(),
+              true,
+              true,
+              message.getDatabase(),
+              insertTabletStatement);
+      if (!event.increaseReferenceCount(MQTTPublishHandler.class.getName())) {
+        LOGGER.warn("The reference count of the event {} cannot be increased, 
skipping it.", event);
+        return;
+      }
+      pendingQueue.waitedOffer(event);
+    } catch (Exception e) {
+      LOGGER.warn(
+          "meet error when polling mqtt source message database {}, table {}, 
tags {}, attributes {}, fields {}, at time {}, because {}",
+          message.getDatabase(),
+          message.getTable(),
+          message.getTagKeys(),
+          message.getAttributeKeys(),
+          message.getFields(),
+          message.getTimestamp(),
+          e.getMessage(),
+          e);
+    }
+  }
+
+  private InsertTabletStatement constructInsertTabletStatement(TableMessage 
message)
+      throws IllegalPathException {
+    InsertTabletStatement statement = new InsertTabletStatement();
+    statement.setDevicePath(
+        
DataNodeDevicePathCache.getInstance().getPartialPath(message.getTable()));
+    List<String> measurements =
+        Stream.of(message.getFields(), message.getTagKeys(), 
message.getAttributeKeys())
+            .flatMap(List::stream)
+            .collect(Collectors.toList());
+    statement.setMeasurements(measurements.toArray(new String[0]));
+    long[] timestamps = new long[] {message.getTimestamp()};
+    statement.setTimes(timestamps);
+    int columnSize = measurements.size();
+    int rowSize = 1;
+
+    BitMap[] bitMaps = new BitMap[columnSize];
+    Object[] columns =
+        Stream.of(message.getValues(), message.getTagValues(), 
message.getAttributeValues())
+            .flatMap(List::stream)
+            .toArray(Object[]::new);
+    statement.setColumns(columns);
+    statement.setBitMaps(bitMaps);
+    statement.setRowCount(rowSize);
+    statement.setAligned(false);
+    statement.setWriteToTable(true);
+    TSDataType[] dataTypes = new TSDataType[measurements.size()];
+    TsTableColumnCategory[] columnCategories = new 
TsTableColumnCategory[measurements.size()];
+    for (int i = 0; i < message.getFields().size(); i++) {
+      dataTypes[i] = message.getDataTypes().get(i);
+      columnCategories[i] = TsTableColumnCategory.FIELD;
+    }
+    for (int i = message.getFields().size();
+        i < message.getFields().size() + message.getTagKeys().size();
+        i++) {
+      dataTypes[i] = TSDataType.STRING;
+      columnCategories[i] = TsTableColumnCategory.TAG;
+    }
+    for (int i = message.getFields().size() + message.getTagKeys().size();
+        i
+            < message.getFields().size()
+                + message.getTagKeys().size()
+                + message.getAttributeKeys().size();
+        i++) {
+      dataTypes[i] = TSDataType.STRING;
+      columnCategories[i] = TsTableColumnCategory.ATTRIBUTE;
+    }
+    statement.setDataTypes(dataTypes);
+    statement.setColumnCategories(columnCategories);
+
+    return statement;
+  }
+
+  private void extractTree(final TreeMessage message, final MqttClientSession 
session) {
+    try {
+      InsertRowStatement statement = new InsertRowStatement();
+      statement.setDevicePath(
+          
DataNodeDevicePathCache.getInstance().getPartialPath(message.getDevice()));
+      TimestampPrecisionUtils.checkTimestampPrecision(message.getTimestamp());
+      statement.setTime(message.getTimestamp());
+      statement.setMeasurements(message.getMeasurements().toArray(new 
String[0]));
+      if (message.getDataTypes() == null) {
+        statement.setDataTypes(new 
TSDataType[message.getMeasurements().size()]);
+        statement.setValues(message.getValues().toArray(new Object[0]));
+        statement.setNeedInferType(true);
+      } else {
+        List<TSDataType> dataTypes = message.getDataTypes();
+        List<String> values = message.getValues();
+        Object[] inferredValues = new Object[values.size()];
+        for (int i = 0; i < values.size(); ++i) {
+          inferredValues[i] =
+              values.get(i) == null
+                  ? null
+                  : CommonUtils.parseValue(dataTypes.get(i), values.get(i));
+        }
+        statement.setDataTypes(dataTypes.toArray(new TSDataType[0]));
+        statement.setValues(inferredValues);
+      }
+      statement.setAligned(false);
+      final EnrichedEvent event =
+          new PipeStatementInsertionEvent(
+              pipeName,
+              creationTime,
+              pipeTaskMeta,
+              null,
+              null,
+              session.getUsername(),
+              true,
+              false,
+              message.getDevice(),
+              statement);
+      if (!event.increaseReferenceCount(MQTTPublishHandler.class.getName())) {
+        LOGGER.warn("The reference count of the event {} cannot be increased, 
skipping it.", event);
+        return;
+      }
+      pendingQueue.waitedOffer(event);
+    } catch (Exception e) {
+      LOGGER.warn(
+          "meet error when polling mqtt source device {}, measurements {}, at 
time {}, because {}",
+          message.getDevice(),
+          message.getMeasurements(),
+          message.getTimestamp(),
+          e.getMessage(),
+          e);
+    }
+  }
+
+  @Override
+  public void onSessionLoopError(Throwable throwable) {
+    LOGGER.warn(
+        "onSessionLoopError: {}",
+        throwable.getMessage() == null ? "null" : throwable.getMessage(),
+        throwable);
+  }
+}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/mqtt/JSONPayloadFormatter.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/mqtt/JSONPayloadFormatter.java
index bbc9e62f389..258c98d6364 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/mqtt/JSONPayloadFormatter.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/mqtt/JSONPayloadFormatter.java
@@ -27,6 +27,7 @@ import com.google.gson.JsonObject;
 import com.google.gson.JsonParseException;
 import com.google.gson.reflect.TypeToken;
 import io.netty.buffer.ByteBuf;
+import org.apache.tsfile.enums.TSDataType;
 
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
@@ -45,6 +46,7 @@ public class JSONPayloadFormatter implements PayloadFormatter 
{
   private static final String JSON_KEY_TIMESTAMPS = "timestamps";
   private static final String JSON_KEY_MEASUREMENTS = "measurements";
   private static final String JSON_KEY_VALUES = "values";
+  private static final String JSON_KEY_DATATYPE = "datatypes";
   private static final Gson GSON = new GsonBuilder().create();
 
   @Override
@@ -88,6 +90,11 @@ public class JSONPayloadFormatter implements 
PayloadFormatter {
             jsonObject.get(JSON_KEY_MEASUREMENTS), new 
TypeToken<List<String>>() {}.getType()));
     message.setValues(
         GSON.fromJson(jsonObject.get(JSON_KEY_VALUES), new 
TypeToken<List<String>>() {}.getType()));
+    if (jsonObject.has(JSON_KEY_DATATYPE)) {
+      message.setDataTypes(
+          GSON.fromJson(
+              jsonObject.get(JSON_KEY_DATATYPE), new 
TypeToken<List<TSDataType>>() {}.getType()));
+    }
     return Lists.newArrayList(message);
   }
 
@@ -103,6 +110,11 @@ public class JSONPayloadFormatter implements 
PayloadFormatter {
     List<List<String>> values =
         GSON.fromJson(
             jsonObject.get(JSON_KEY_VALUES), new 
TypeToken<List<List<String>>>() {}.getType());
+    List<TSDataType> types =
+        jsonObject.has(JSON_KEY_DATATYPE)
+            ? GSON.fromJson(
+                jsonObject.get(JSON_KEY_DATATYPE), new 
TypeToken<List<TSDataType>>() {}.getType())
+            : null;
 
     List<Message> ret = new ArrayList<>(timestamps.size());
     for (int i = 0; i < timestamps.size(); i++) {
@@ -111,6 +123,7 @@ public class JSONPayloadFormatter implements 
PayloadFormatter {
       message.setTimestamp(timestamps.get(i));
       message.setMeasurements(measurements);
       message.setValues(values.get(i));
+      message.setDataTypes(types);
       ret.add(message);
     }
     return ret;
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/executor/ClusterConfigTaskExecutor.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/executor/ClusterConfigTaskExecutor.java
index 78304754418..5d284187ca6 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/executor/ClusterConfigTaskExecutor.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/executor/ClusterConfigTaskExecutor.java
@@ -2077,6 +2077,18 @@ public class ClusterConfigTaskExecutor implements 
IConfigTaskExecutor {
     final Map<String, String> connectorAttributes;
     try {
       if (!alterPipeStatement.getExtractorAttributes().isEmpty()) {
+        // We don't allow changing the extractor plugin type
+        if (alterPipeStatement
+                .getExtractorAttributes()
+                .containsKey(PipeExtractorConstant.EXTRACTOR_KEY)
+            || alterPipeStatement
+                .getExtractorAttributes()
+                .containsKey(PipeExtractorConstant.SOURCE_KEY)
+            || alterPipeStatement.isReplaceAllExtractorAttributes()) {
+          checkIfSourcePluginChanged(
+              pipeMetaFromCoordinator.getStaticMeta().getExtractorParameters(),
+              new PipeParameters(alterPipeStatement.getExtractorAttributes()));
+        }
         if (alterPipeStatement.isReplaceAllExtractorAttributes()) {
           extractorAttributes = alterPipeStatement.getExtractorAttributes();
         } else {
@@ -2170,6 +2182,30 @@ public class ClusterConfigTaskExecutor implements 
IConfigTaskExecutor {
     return future;
   }
 
+  private static void checkIfSourcePluginChanged(
+      final PipeParameters oldPipeParameters, final PipeParameters 
newPipeParameters) {
+    final String oldPluginName =
+        oldPipeParameters
+            .getStringOrDefault(
+                Arrays.asList(
+                    PipeExtractorConstant.EXTRACTOR_KEY, 
PipeExtractorConstant.SOURCE_KEY),
+                BuiltinPipePlugin.IOTDB_EXTRACTOR.getPipePluginName())
+            .toLowerCase();
+    final String newPluginName =
+        newPipeParameters
+            .getStringOrDefault(
+                Arrays.asList(
+                    PipeExtractorConstant.EXTRACTOR_KEY, 
PipeExtractorConstant.SOURCE_KEY),
+                BuiltinPipePlugin.IOTDB_EXTRACTOR.getPipePluginName())
+            .toLowerCase();
+    if (!PipeDataNodeAgent.plugin().checkIfPluginSameType(oldPluginName, 
newPluginName)) {
+      throw new SemanticException(
+          String.format(
+              "Failed to alter pipe, the source plugin of the pipe cannot be 
changed from %s to %s",
+              oldPluginName, newPluginName));
+    }
+  }
+
   private static void checkSourceType(
       final String pipeName, final Map<String, String> 
replacedExtractorAttributes) {
     final PipeParameters extractorParameters = new 
PipeParameters(replacedExtractorAttributes);
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/plugin/builtin/BuiltinPipePlugin.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/plugin/builtin/BuiltinPipePlugin.java
index 0f2e39f2450..44fd9037532 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/plugin/builtin/BuiltinPipePlugin.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/plugin/builtin/BuiltinPipePlugin.java
@@ -33,6 +33,7 @@ import 
org.apache.iotdb.commons.pipe.agent.plugin.builtin.connector.websocket.We
 import 
org.apache.iotdb.commons.pipe.agent.plugin.builtin.connector.writeback.WriteBackConnector;
 import 
org.apache.iotdb.commons.pipe.agent.plugin.builtin.extractor.donothing.DoNothingExtractor;
 import 
org.apache.iotdb.commons.pipe.agent.plugin.builtin.extractor.iotdb.IoTDBExtractor;
+import 
org.apache.iotdb.commons.pipe.agent.plugin.builtin.extractor.mqtt.MQTTExtractor;
 import 
org.apache.iotdb.commons.pipe.agent.plugin.builtin.processor.aggregate.AggregateProcessor;
 import 
org.apache.iotdb.commons.pipe.agent.plugin.builtin.processor.aggregate.StandardStatisticsProcessor;
 import 
org.apache.iotdb.commons.pipe.agent.plugin.builtin.processor.aggregate.TumblingWindowingProcessor;
@@ -55,9 +56,11 @@ public enum BuiltinPipePlugin {
   // extractors
   DO_NOTHING_EXTRACTOR("do-nothing-extractor", DoNothingExtractor.class),
   IOTDB_EXTRACTOR("iotdb-extractor", IoTDBExtractor.class),
+  MQTT_EXTRACTOR("mqtt-extractor", MQTTExtractor.class),
 
   DO_NOTHING_SOURCE("do-nothing-source", DoNothingExtractor.class),
   IOTDB_SOURCE("iotdb-source", IoTDBExtractor.class),
+  MQTT_SOURCE("mqtt-source", MQTTExtractor.class),
 
   // processors
   DO_NOTHING_PROCESSOR("do-nothing-processor", DoNothingProcessor.class),
@@ -129,6 +132,16 @@ public enum BuiltinPipePlugin {
     return className;
   }
 
+  // used to distinguish between builtin and external sources
+  public static final Set<String> BUILTIN_SOURCES =
+      Collections.unmodifiableSet(
+          new HashSet<>(
+              Arrays.asList(
+                  DO_NOTHING_EXTRACTOR.getPipePluginName().toLowerCase(),
+                  IOTDB_EXTRACTOR.getPipePluginName().toLowerCase(),
+                  DO_NOTHING_SOURCE.getPipePluginName().toLowerCase(),
+                  IOTDB_SOURCE.getPipePluginName().toLowerCase())));
+
   public static final Set<String> SHOW_PIPE_PLUGINS_BLACKLIST =
       Collections.unmodifiableSet(
           new HashSet<>(
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/plugin/builtin/extractor/mqtt/MQTTExtractor.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/plugin/builtin/extractor/mqtt/MQTTExtractor.java
new file mode 100644
index 00000000000..7bc261d13aa
--- /dev/null
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/plugin/builtin/extractor/mqtt/MQTTExtractor.java
@@ -0,0 +1,68 @@
+/*
+ * 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.iotdb.commons.pipe.agent.plugin.builtin.extractor.mqtt;
+
+import org.apache.iotdb.pipe.api.PipeExtractor;
+import org.apache.iotdb.pipe.api.annotation.TableModel;
+import org.apache.iotdb.pipe.api.annotation.TreeModel;
+import 
org.apache.iotdb.pipe.api.customizer.configuration.PipeExtractorRuntimeConfiguration;
+import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameterValidator;
+import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameters;
+import org.apache.iotdb.pipe.api.event.Event;
+
+/**
+ * This class is a placeholder and should not be initialized. It represents an 
external Extractor
+ * that uses the MQTT protocol to receive data. There is a real implementation 
in the server module
+ * but cannot be imported here. The pipe agent in the server module will 
replace this class with the
+ * real implementation when initializing the MQTTExtractor.
+ */
+@TreeModel
+@TableModel
+public class MQTTExtractor implements PipeExtractor {
+
+  private static final String PLACEHOLDER_ERROR_MSG =
+      "This class is a placeholder and should not be used.";
+
+  @Override
+  public void validate(PipeParameterValidator validator) throws Exception {
+    throw new UnsupportedOperationException(PLACEHOLDER_ERROR_MSG);
+  }
+
+  @Override
+  public void customize(PipeParameters parameters, 
PipeExtractorRuntimeConfiguration configuration)
+      throws Exception {
+    throw new UnsupportedOperationException(PLACEHOLDER_ERROR_MSG);
+  }
+
+  @Override
+  public void start() throws Exception {
+    throw new UnsupportedOperationException(PLACEHOLDER_ERROR_MSG);
+  }
+
+  @Override
+  public Event supply() throws Exception {
+    throw new UnsupportedOperationException(PLACEHOLDER_ERROR_MSG);
+  }
+
+  @Override
+  public void close() throws Exception {
+    throw new UnsupportedOperationException(PLACEHOLDER_ERROR_MSG);
+  }
+}
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/task/meta/PipeRuntimeMeta.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/task/meta/PipeRuntimeMeta.java
index 5223b6a9e8a..e4beaf20bbf 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/task/meta/PipeRuntimeMeta.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/task/meta/PipeRuntimeMeta.java
@@ -136,6 +136,16 @@ public class PipeRuntimeMeta {
     this.isStoppedByRuntimeException.set(isStoppedByRuntimeException);
   }
 
+  /**
+   * We use negative regionId to identify the external pipe source, which is 
not a consensus group
+   * id. Then we can reuse the regionId to schedule the external pipe source 
and store the progress
+   * information.
+   */
+  public static boolean isSourceExternal(int regionId) {
+    // regionId that is less than 0 is a special value marking an external 
pipe source
+    return regionId < 0;
+  }
+
   public ByteBuffer serialize() throws IOException {
     PublicBAOS byteArrayOutputStream = new PublicBAOS();
     DataOutputStream outputStream = new 
DataOutputStream(byteArrayOutputStream);
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/task/meta/PipeStaticMeta.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/task/meta/PipeStaticMeta.java
index 92dd20c0778..f82627e256b 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/task/meta/PipeStaticMeta.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/agent/task/meta/PipeStaticMeta.java
@@ -19,28 +19,27 @@
 
 package org.apache.iotdb.commons.pipe.agent.task.meta;
 
+import org.apache.iotdb.commons.pipe.agent.plugin.builtin.BuiltinPipePlugin;
+import org.apache.iotdb.commons.pipe.config.constant.PipeExtractorConstant;
 import org.apache.iotdb.commons.pipe.datastructure.visibility.Visibility;
 import org.apache.iotdb.commons.pipe.datastructure.visibility.VisibilityUtils;
 import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameters;
 
 import org.apache.tsfile.utils.PublicBAOS;
 import org.apache.tsfile.utils.ReadWriteIOUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.DataOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.ByteBuffer;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
 
 public class PipeStaticMeta {
 
-  private static final Logger LOGGER = 
LoggerFactory.getLogger(PipeStaticMeta.class);
-
   private String pipeName;
   private long creationTime;
 
@@ -89,6 +88,16 @@ public class PipeStaticMeta {
     return PipeType.getPipeType(pipeName);
   }
 
+  public boolean isSourceExternal() {
+    return !BuiltinPipePlugin.BUILTIN_SOURCES.contains(
+        extractorParameters
+            .getStringOrDefault(
+                Arrays.asList(
+                    PipeExtractorConstant.EXTRACTOR_KEY, 
PipeExtractorConstant.SOURCE_KEY),
+                BuiltinPipePlugin.IOTDB_EXTRACTOR.getPipePluginName())
+            .toLowerCase());
+  }
+
   public ByteBuffer serialize() throws IOException {
     PublicBAOS byteArrayOutputStream = new PublicBAOS();
     DataOutputStream outputStream = new 
DataOutputStream(byteArrayOutputStream);
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/config/constant/PipeConnectorConstant.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/config/constant/PipeConnectorConstant.java
index 8eabf50406a..9e9c6606dcd 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/config/constant/PipeConnectorConstant.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/config/constant/PipeConnectorConstant.java
@@ -264,6 +264,10 @@ public class PipeConnectorConstant {
   public static final String CONNECTOR_OPC_DA_PROGID_KEY = 
"connector.opcda.progid";
   public static final String SINK_OPC_DA_PROGID_KEY = "sink.opcda.progid";
 
+  public static final String CONNECTOR_USE_EVENT_USER_NAME_KEY = 
"connector.use-event-user-name";
+  public static final String SINK_USE_EVENT_USER_NAME_KEY = 
"sink.use-event-user-name";
+  public static final boolean CONNECTOR_USE_EVENT_USER_NAME_DEFAULT_VALUE = 
false;
+
   private PipeConnectorConstant() {
     throw new IllegalStateException("Utility class");
   }
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/config/constant/PipeExtractorConstant.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/config/constant/PipeExtractorConstant.java
index b1804b7e18b..2fb4a71f0d7 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/config/constant/PipeExtractorConstant.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/pipe/config/constant/PipeExtractorConstant.java
@@ -19,6 +19,11 @@
 
 package org.apache.iotdb.commons.pipe.config.constant;
 
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
 public class PipeExtractorConstant {
 
   public static final String EXTRACTOR_KEY = "extractor";
@@ -147,6 +152,51 @@ public class PipeExtractorConstant {
   public static final String SOURCE_SKIP_IF_KEY = "source.skipif";
   public static final String EXTRACTOR_IOTDB_SKIP_IF_NO_PRIVILEGES = 
"no-privileges";
 
+  ////////////////// external sources ////////////////
+
+  public static final String EXTERNAL_EXTRACTOR_BALANCE_STRATEGY_KEY = 
"extractor.balance-strategy";
+  public static final String EXTERNAL_SOURCE_BALANCE_STRATEGY_KEY = 
"source.balance-strategy";
+  public static final String EXTERNAL_EXTRACTOR_BALANCE_PROPORTION_STRATEGY = 
"proportion";
+  public static final Set<String> EXTERNAL_EXTRACTOR_BALANCE_STRATEGY_SET =
+      Collections.unmodifiableSet(
+          new 
HashSet<>(Arrays.asList(EXTERNAL_EXTRACTOR_BALANCE_PROPORTION_STRATEGY)));
+
+  public static final String EXTERNAL_EXTRACTOR_PARALLELISM_KEY = 
"extractor.parallelism";
+  public static final String EXTERNAL_SOURCE_PARALLELISM_KEY = 
"source.parallelism";
+  public static final int EXTERNAL_EXTRACTOR_PARALLELISM_DEFAULT_VALUE = 1;
+
+  public static final String EXTERNAL_EXTRACTOR_SINGLE_INSTANCE_PER_NODE_KEY =
+      "extractor.single-mode";
+  public static final String EXTERNAL_SOURCE_SINGLE_INSTANCE_PER_NODE_KEY = 
"source.single-mode";
+  public static final boolean 
EXTERNAL_EXTRACTOR_SINGLE_INSTANCE_PER_NODE_DEFAULT_VALUE = true;
+
+  public static final String MQTT_BROKER_HOST_KEY = "mqtt.host";
+  public static final String MQTT_BROKER_HOST_DEFAULT_VALUE = "127.0.0.1";
+  public static final String MQTT_BROKER_PORT_KEY = "mqtt.port";
+  public static final String MQTT_BROKER_PORT_DEFAULT_VALUE = "1883";
+
+  public static final String MQTT_BROKER_INTERCEPTOR_THREAD_POOL_SIZE_KEY = 
"mqtt.pool-size";
+  public static final int 
MQTT_BROKER_INTERCEPTOR_THREAD_POOL_SIZE_DEFAULT_VALUE = 1;
+
+  public static final String MQTT_DATA_PATH_PROPERTY_NAME_KEY = 
"mqtt.data-path";
+  public static final String MQTT_DATA_PATH_PROPERTY_NAME_DEFAULT_VALUE = 
"data/";
+
+  public static final String MQTT_IMMEDIATE_BUFFER_FLUSH_PROPERTY_NAME_KEY = 
"mqtt.immediate-flush";
+  public static final boolean 
MQTT_IMMEDIATE_BUFFER_FLUSH_PROPERTY_NAME_DEFAULT_VALUE = true;
+
+  public static final String MQTT_ALLOW_ANONYMOUS_PROPERTY_NAME_KEY = 
"mqtt.allow-anonymous";
+  public static final boolean MQTT_ALLOW_ANONYMOUS_PROPERTY_NAME_DEFAULT_VALUE 
= false;
+
+  public static final String MQTT_ALLOW_ZERO_BYTE_CLIENT_ID_PROPERTY_NAME_KEY =
+      "mqtt.allow-zero-byte-client-id";
+  public static final boolean 
MQTT_ALLOW_ZERO_BYTE_CLIENT_ID_PROPERTY_NAME_DEFAULT_VALUE = true;
+
+  public static final String MQTT_NETTY_MAX_BYTES_PROPERTY_NAME_KEY = 
"mqtt.max-message-size";
+  public static final long MQTT_NETTY_MAX_BYTES_PROPERTY_NAME_DEFAULT_VALUE = 
1048576;
+
+  public static final String MQTT_PAYLOAD_FORMATTER_KEY = 
"mqtt.payload-formatter";
+  public static final String MQTT_PAYLOAD_FORMATTER_DEFAULT_VALUE = "json";
+
   ///////////////////// pipe consensus /////////////////////
 
   public static final String EXTRACTOR_CONSENSUS_GROUP_ID_KEY = 
"extractor.consensus.group-id";

Reply via email to