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

jt2594838 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 595aa8442bb Fix write-back sink history failure state handling (#19056)
595aa8442bb is described below

commit 595aa8442bb50fc8bfafdb8f11d40a94e1b3c1bd
Author: Caideyipi <[email protected]>
AuthorDate: Mon Nov 2 10:08:10 2026 +0800

    Fix write-back sink history failure state handling (#19056)
    
    * Fix write-back sink history failure state handling
    
    * Fix write-back sink auto-split test credentials
    
    * Fix pipe metadata retry persistence
---
 .../treemodel/auto/basic/IoTDBPipeAutoSplitIT.java |  56 +++++
 .../parameter/PipeParameterValidator.java          |  24 ++-
 .../parameter/PipeParameterValidatorTest.java      | 126 +++++++++++
 .../iotdb/confignode/i18n/ProcedureMessages.java   |   2 +
 .../iotdb/confignode/i18n/ProcedureMessages.java   |   2 +
 .../confignode/persistence/pipe/PipeTaskInfo.java  | 121 ++++++++---
 .../impl/pipe/AbstractOperatePipeProcedureV2.java  |  80 +++++--
 .../impl/pipe/runtime/PipeMetaSyncProcedure.java   |   4 +
 .../impl/pipe/task/AlterPipeProcedureV2.java       |   9 +-
 .../impl/pipe/task/CreatePipeProcedureV2.java      |  12 +-
 .../impl/pipe/task/StartPipeProcedureV2.java       |  59 +++--
 .../consensus/response/pipe/PipeTableRespTest.java |  20 ++
 .../pipe/PipeTaskInfoAutoRestartTest.java          |  54 +++++
 .../pipe/AbstractOperatePipeProcedureV2Test.java   |  24 +++
 .../pipe/runtime/PipeMetaSyncProcedureTest.java    | 240 +++++++++++++++++++++
 .../impl/pipe/task/CreatePipeProcedureV2Test.java  | 146 +++++++++++++
 .../impl/pipe/task/StartPipeProcedureV2Test.java   | 217 +++++++++++++++++++
 .../apache/iotdb/db/pipe/sink/PipeSinkTest.java    |  31 +++
 .../config/sys/pipe/ShowPipeTaskTest.java          |   7 +
 19 files changed, 1143 insertions(+), 91 deletions(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/pipe/it/dual/treemodel/auto/basic/IoTDBPipeAutoSplitIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/pipe/it/dual/treemodel/auto/basic/IoTDBPipeAutoSplitIT.java
index 4b9012b88ab..c737ce32ec7 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/pipe/it/dual/treemodel/auto/basic/IoTDBPipeAutoSplitIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/pipe/it/dual/treemodel/auto/basic/IoTDBPipeAutoSplitIT.java
@@ -20,6 +20,7 @@
 package org.apache.iotdb.pipe.it.dual.treemodel.auto.basic;
 
 import org.apache.iotdb.commons.client.sync.SyncConfigNodeIServiceClient;
+import org.apache.iotdb.commons.pipe.agent.task.meta.PipeStatus;
 import org.apache.iotdb.confignode.rpc.thrift.TShowPipeInfo;
 import org.apache.iotdb.confignode.rpc.thrift.TShowPipeReq;
 import org.apache.iotdb.consensus.ConsensusFactory;
@@ -42,6 +43,7 @@ import java.sql.SQLException;
 import java.sql.Statement;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
 
@@ -82,6 +84,60 @@ public class IoTDBPipeAutoSplitIT extends 
AbstractPipeDualTreeModelAutoIT {
     receiverEnv.initClusterEnvironment();
   }
 
+  @Test
+  public void testWriteBackSinkWithTargetDatabase() throws Exception {
+    TestUtils.executeNonQueries(
+        senderEnv,
+        Arrays.asList(
+            "create database root.writeback_source",
+            "create aligned timeseries root.writeback_source.d1(s1 INT32)",
+            "insert into root.writeback_source.d1(time,s1) aligned values "
+                + "(1,1),(2,2),(3,3),(4,4),(5,5)",
+            "flush",
+            "create pipe writeback with source ("
+                + 
"'path'='root.writeback_source.**','inclusion'='data.insert',"
+                + "'forwarding-pipe-requests'='false',"
+                + "'history.enable'='true','realtime.enable'='true') "
+                + "with sink 
('sink'='write-back-sink','sink.database'='root.writeback_target',"
+                + "'user'='root','password'='root')"),
+        null);
+
+    TestUtils.assertDataEventuallyOnEnv(
+        senderEnv,
+        "select s1 from root.writeback_target.d1",
+        "Time,root.writeback_target.d1.s1,",
+        new HashSet<>(Arrays.asList("1,1,", "2,2,", "3,3,", "4,4,", "5,5,")),
+        90);
+
+    try (final SyncConfigNodeIServiceClient client =
+        (SyncConfigNodeIServiceClient) 
senderEnv.getLeaderConfigNodeConnection()) {
+      final List<TShowPipeInfo> pipeInfoList =
+          client.showPipe(new 
TShowPipeReq().setUserName(SessionConfig.DEFAULT_USER)).pipeInfoList;
+      final TShowPipeInfo realtimePipe =
+          pipeInfoList.stream()
+              .filter(pipeInfo -> "writeback_realtime".equals(pipeInfo.id))
+              .findFirst()
+              .orElseThrow(AssertionError::new);
+      Assert.assertEquals(PipeStatus.RUNNING.name(), realtimePipe.getState());
+      Assert.assertEquals("", realtimePipe.getExceptionMessage());
+    }
+
+    TestUtils.executeNonQueries(
+        senderEnv,
+        Collections.singletonList(
+            "insert into root.writeback_source.d1(time,s1) aligned values "
+                + "(6,6),(7,7),(8,8),(9,9)"),
+        null);
+
+    TestUtils.assertDataEventuallyOnEnv(
+        senderEnv,
+        "select s1 from root.writeback_target.d1",
+        "Time,root.writeback_target.d1.s1,",
+        new HashSet<>(
+            Arrays.asList("1,1,", "2,2,", "3,3,", "4,4,", "5,5,", "6,6,", 
"7,7,", "8,8,", "9,9,")),
+        90);
+  }
+
   @Test
   public void testSingleEnv() throws Exception {
     final DataNodeWrapper receiverDataNode = receiverEnv.getDataNodeWrapper(0);
diff --git 
a/iotdb-api/pipe-api/src/main/java/org/apache/iotdb/pipe/api/customizer/parameter/PipeParameterValidator.java
 
b/iotdb-api/pipe-api/src/main/java/org/apache/iotdb/pipe/api/customizer/parameter/PipeParameterValidator.java
index 57894ed244b..3e0562bf453 100644
--- 
a/iotdb-api/pipe-api/src/main/java/org/apache/iotdb/pipe/api/customizer/parameter/PipeParameterValidator.java
+++ 
b/iotdb-api/pipe-api/src/main/java/org/apache/iotdb/pipe/api/customizer/parameter/PipeParameterValidator.java
@@ -26,6 +26,7 @@ import org.apache.iotdb.pipe.api.i18n.PipeApiMessages;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -58,9 +59,14 @@ public class PipeParameterValidator {
       final List<String> lhsAttributes,
       final List<String> rhsAttributes,
       final boolean isRequired) {
-    final boolean lhsExistence = 
lhsAttributes.stream().anyMatch(parameters::hasAttribute);
-    final boolean rhsExistence = 
rhsAttributes.stream().anyMatch(parameters::hasAttribute);
-    if (lhsExistence && rhsExistence) {
+    final Set<String> lhsExistingKeys = 
getExistingAttributeKeys(lhsAttributes);
+    final Set<String> rhsExistingKeys = 
getExistingAttributeKeys(rhsAttributes);
+    final boolean lhsExistence = !lhsExistingKeys.isEmpty();
+    final boolean rhsExistence = !rhsExistingKeys.isEmpty();
+    // One unprefixed key may match both sides after prefix reduction. It is 
still one attribute.
+    if (lhsExistence
+        && rhsExistence
+        && !(lhsExistingKeys.size() == 1 && 
lhsExistingKeys.equals(rhsExistingKeys))) {
       throw new PipeParameterNotValidException(
           String.format(
               
PipeApiMessages.EXCEPTION_CANNOT_SPECIFY_BOTH_ARG_AND_ARG_AT_THE_SAME_TIME_7DA8858B,
@@ -77,6 +83,18 @@ public class PipeParameterValidator {
     return this;
   }
 
+  private Set<String> getExistingAttributeKeys(final List<String> 
attributeKeys) {
+    return attributeKeys.stream()
+        .flatMap(
+            key ->
+                Stream.of(
+                    key,
+                    PipeParameters.KeyReducer.shallowReduce(key),
+                    PipeParameters.KeyReducer.reduce(key)))
+        .filter(parameters.getAttribute()::containsKey)
+        .collect(Collectors.toSet());
+  }
+
   /**
    * Validates whether the attributes entered by the user contain an attribute 
whose key is
    * attributeKey.
diff --git 
a/iotdb-api/pipe-api/src/test/java/org/apache/iotdb/pipe/api/customizer/parameter/PipeParameterValidatorTest.java
 
b/iotdb-api/pipe-api/src/test/java/org/apache/iotdb/pipe/api/customizer/parameter/PipeParameterValidatorTest.java
new file mode 100644
index 00000000000..5c5e6a1a07a
--- /dev/null
+++ 
b/iotdb-api/pipe-api/src/test/java/org/apache/iotdb/pipe/api/customizer/parameter/PipeParameterValidatorTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.pipe.api.customizer.parameter;
+
+import org.apache.iotdb.pipe.api.exception.PipeAttributeNotProvidedException;
+import org.apache.iotdb.pipe.api.exception.PipeParameterNotValidException;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class PipeParameterValidatorTest {
+
+  @Test
+  public void testDatabaseAliasesMatchOneAttribute() {
+    for (final String key : Arrays.asList("connector.database", 
"sink.database", "database")) {
+      final PipeParameterValidator validator =
+          new PipeParameterValidator(
+              new PipeParameters(Collections.singletonMap(key, 
"root.target")));
+      for (final boolean required : new boolean[] {false, true}) {
+        Assert.assertSame(
+            validator,
+            validator.validateSynonymAttributes(
+                Collections.singletonList("connector.database"),
+                Collections.singletonList("sink.database"),
+                required));
+      }
+    }
+  }
+
+  @Test
+  public void testDatabaseAliasesRejectDistinctAttributes() {
+    final String[] keys = {"connector.database", "sink.database", "database"};
+    for (int i = 0; i < keys.length; i++) {
+      for (int j = i + 1; j < keys.length; j++) {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put(keys[i], "root.target");
+        attributes.put(keys[j], "root.target");
+        Assert.assertThrows(
+            PipeParameterNotValidException.class,
+            () ->
+                new PipeParameterValidator(new PipeParameters(attributes))
+                    .validateSynonymAttributes(
+                        Collections.singletonList("connector.database"),
+                        Collections.singletonList("sink.database"),
+                        false));
+      }
+    }
+  }
+
+  @Test
+  public void testDistinctSynonymsRemainMutuallyExclusive() {
+    for (final String userKey : Arrays.asList("connector.user", "sink.user", 
"user")) {
+      for (final String usernameKey :
+          Arrays.asList("connector.username", "sink.username", "username")) {
+        final Map<String, String> attributes = new HashMap<>();
+        attributes.put(userKey, "user");
+        attributes.put(usernameKey, "user");
+        Assert.assertThrows(
+            PipeParameterNotValidException.class,
+            () ->
+                new PipeParameterValidator(new PipeParameters(attributes))
+                    .validateSynonymAttributes(
+                        Arrays.asList("connector.user", "sink.user"),
+                        Arrays.asList("connector.username", "sink.username"),
+                        false));
+      }
+    }
+  }
+
+  @Test
+  public void testShallowPrefixesMatchOneAttribute() {
+    new PipeParameterValidator(
+            new PipeParameters(Collections.singletonMap("opcua.database", 
"root.target")))
+        .validateSynonymAttributes(
+            Collections.singletonList("connector.opcua.database"),
+            Collections.singletonList("sink.opcua.database"),
+            true);
+  }
+
+  @Test
+  public void testRequiredSynonymsSupportUnprefixedAttributes() {
+    for (final String key : Arrays.asList("output.series", "output-series")) {
+      new PipeParameterValidator(new 
PipeParameters(Collections.singletonMap(key, "root.db.s")))
+          .validateSynonymAttributes(
+              Collections.singletonList("processor.output.series"),
+              Collections.singletonList("processor.output-series"),
+              true);
+    }
+
+    final PipeParameterValidator validator =
+        new PipeParameterValidator(new PipeParameters(Collections.emptyMap()));
+    validator.validateSynonymAttributes(
+        Collections.singletonList("connector.database"),
+        Collections.singletonList("sink.database"),
+        false);
+    Assert.assertThrows(
+        PipeAttributeNotProvidedException.class,
+        () ->
+            validator.validateSynonymAttributes(
+                Collections.singletonList("connector.database"),
+                Collections.singletonList("sink.database"),
+                true));
+  }
+}
diff --git 
a/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ProcedureMessages.java
 
b/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ProcedureMessages.java
index 46a278848c9..79b02325d7a 100644
--- 
a/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ProcedureMessages.java
+++ 
b/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ProcedureMessages.java
@@ -454,6 +454,8 @@ public final class ProcedureMessages {
       "Failed to push pipe meta list to data nodes, will retry later.";
   public static final String FAILED_TO_PUSH_PIPE_META_TO_DATANODES_DETAILS =
       "Failed to push pipe meta to dataNodes, details: %s";
+  public static final String 
EXCEPTION_INTERNAL_ERROR_WHILE_PROCESSING_PUSHPIPEMETA_ON_DATANODE_5983BD5F =
+      "Internal error while processing pushPipeMeta on DataNode.";
   public static final String FAILED_TO_PUSH_TOPIC_META_TO_DATANODES_DETAILS =
       "Failed to push topic meta to dataNodes, details: %s";
   public static final String FAILED_TO_REMOVE_DATA_NODE_BECAUSE_IT_IS_NOT_IN =
diff --git 
a/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ProcedureMessages.java
 
b/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ProcedureMessages.java
index e972e688ae6..c10a30c65f5 100644
--- 
a/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ProcedureMessages.java
+++ 
b/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ProcedureMessages.java
@@ -439,6 +439,8 @@ public final class ProcedureMessages {
       "向 DataNode 推送 pipe 元数据列表失败,将稍后重试。";
   public static final String FAILED_TO_PUSH_PIPE_META_TO_DATANODES_DETAILS =
       "向 DataNode 推送 pipe 元数据失败,详情:%s";
+  public static final String 
EXCEPTION_INTERNAL_ERROR_WHILE_PROCESSING_PUSHPIPEMETA_ON_DATANODE_5983BD5F =
+      "处理 DataNode 上的 pushPipeMeta 时发生内部错误。";
   public static final String FAILED_TO_PUSH_TOPIC_META_TO_DATANODES_DETAILS =
       "向 DataNode 推送 topic 元数据失败,详情:%s";
   public static final String FAILED_TO_REMOVE_DATA_NODE_BECAUSE_IT_IS_NOT_IN =
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 2427e7f3329..78040be82d5 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
@@ -55,6 +55,7 @@ import 
org.apache.iotdb.confignode.consensus.request.write.pipe.task.SetPipeStat
 import 
org.apache.iotdb.confignode.consensus.request.write.pipe.task.SetPipeStatusWithStoppedByRuntimeExceptionPlanV2;
 import org.apache.iotdb.confignode.consensus.response.pipe.task.PipeTableResp;
 import org.apache.iotdb.confignode.i18n.ConfigNodeMessages;
+import org.apache.iotdb.confignode.i18n.ProcedureMessages;
 import 
org.apache.iotdb.confignode.manager.pipe.resource.PipeConfigNodeResourceManager;
 import 
org.apache.iotdb.confignode.procedure.impl.pipe.runtime.PipeHandleMetaChangeProcedure;
 import org.apache.iotdb.confignode.rpc.thrift.TAlterPipeReq;
@@ -97,6 +98,9 @@ public class PipeTaskInfo implements SnapshotProcessor {
 
   private final PipeMetaKeeper pipeMetaKeeper;
 
+  // Tracks runtime exception changes that have not yet been persisted through 
consensus.
+  private volatile boolean pipeMetaExceptionPersistencePending;
+
   // Pure in-memory object, not involved in snapshot serialization and 
deserialization.
   private final PipeTaskInfoVersion pipeTaskInfoVersion;
   // Accepts a username and returns its current stored password for pipe 
authentication.
@@ -1138,22 +1142,43 @@ public class PipeTaskInfo implements SnapshotProcessor {
    *
    * <p>If there are exceptions recorded, the related pipes will be stopped, 
and the exception
    * messages will then be updated to all the nodes through {@link 
PipeHandleMetaChangeProcedure}.
+   * Failures without a pipe identity apply to every pipe in the full metadata 
sync.
    *
    * @param respMap The responseMap after pushing pipe meta
    * @return {@code true} if there are exceptions encountered
    */
   public boolean recordDataNodePushPipeMetaExceptions(
       final Map<Integer, TPushPipeMetaResp> respMap) {
+    return recordDataNodePushPipeMetaExceptions(respMap, null);
+  }
+
+  /**
+   * Record push failures locally, using the given pipe meta when a DataNode 
cannot identify the
+   * pipe in its response.
+   */
+  public boolean recordDataNodePushPipeMetaExceptions(
+      final Map<Integer, TPushPipeMetaResp> respMap, final PipeStaticMeta 
fallbackPipeStaticMeta) {
     acquireWriteLock();
     try {
-      return recordDataNodePushPipeMetaExceptionsInternal(respMap);
+      final boolean hasException =
+          recordDataNodePushPipeMetaExceptionsInternal(respMap, 
fallbackPipeStaticMeta);
+      pipeMetaExceptionPersistencePending |= hasException;
+      return hasException;
     } finally {
       releaseWriteLock();
     }
   }
 
+  public boolean hasPendingPipeMetaExceptionPersistence() {
+    return pipeMetaExceptionPersistencePending;
+  }
+
+  public void markPipeMetaExceptionsPersisted() {
+    pipeMetaExceptionPersistencePending = false;
+  }
+
   private boolean recordDataNodePushPipeMetaExceptionsInternal(
-      final Map<Integer, TPushPipeMetaResp> respMap) {
+      final Map<Integer, TPushPipeMetaResp> respMap, final PipeStaticMeta 
fallbackPipeStaticMeta) {
     boolean hasException = false;
 
     for (final Map.Entry<Integer, TPushPipeMetaResp> respEntry : 
respMap.entrySet()) {
@@ -1163,46 +1188,36 @@ public class PipeTaskInfo implements SnapshotProcessor {
       if (resp.getStatus().getCode() == 
TSStatusCode.PIPE_PUSH_META_ERROR.getStatusCode()) {
         hasException = true;
 
-        if (!resp.isSetExceptionMessages()) {
-          // The pushPipeMeta process on dataNode encountered internal errors
+        if (!resp.isSetExceptionMessages() || 
resp.getExceptionMessages().isEmpty()) {
+          final String statusMessage = resp.getStatus().getMessage();
+          recordUnscopedDataNodePushPipeMetaException(
+              fallbackPipeStaticMeta,
+              dataNodeId,
+              statusMessage == null || statusMessage.isEmpty()
+                  ? ProcedureMessages
+                      
.EXCEPTION_INTERNAL_ERROR_WHILE_PROCESSING_PUSHPIPEMETA_ON_DATANODE_5983BD5F
+                  : statusMessage,
+              System.currentTimeMillis());
           continue;
         }
 
         for (final TPushPipeMetaRespExceptionMessage message : 
resp.getExceptionMessages()) {
+          final String messagePipeName = message.getPipeName();
+          if (messagePipeName == null || messagePipeName.isEmpty()) {
+            recordUnscopedDataNodePushPipeMetaException(
+                fallbackPipeStaticMeta, dataNodeId, message.getMessage(), 
message.getTimeStamp());
+            continue;
+          }
           final PipeMeta pipeMeta =
               message.isSetCreationTime()
-                  ? pipeMetaKeeper.getPipeMeta(message.getPipeName(), 
message.getCreationTime())
-                  : pipeMetaKeeper.getPipeMeta(message.getPipeName());
+                  ? pipeMetaKeeper.getPipeMeta(messagePipeName, 
message.getCreationTime())
+                  : pipeMetaKeeper.getPipeMeta(messagePipeName);
           if (pipeMeta == null) {
             continue;
           }
 
-          final PipeRuntimeMeta runtimeMeta = pipeMeta.getRuntimeMeta();
-
-          if (PipeStatus.PRE_DELETE.equals(runtimeMeta.getStatus().get())) {
-            continue;
-          }
-
-          // Keep user-stopped pipes out of the auto-restart flow. Otherwise, 
a failed STOPPED meta
-          // sync can turn a manually stopped pipe into a runtime-stopped one 
and the next
-          // PipeMetaSyncer round will restart it automatically.
-          if (PipeStatus.STOPPED.equals(runtimeMeta.getStatus().get())
-              && !runtimeMeta.getIsStoppedByRuntimeException()) {
-            continue;
-          }
-
-          // Mark the status of the pipe with exception as stopped
-          runtimeMeta.getStatus().set(PipeStatus.STOPPED);
-          runtimeMeta.setIsStoppedByRuntimeException(true);
-
-          final Map<Integer, PipeRuntimeException> exceptionMap =
-              runtimeMeta.getNodeId2PipeRuntimeExceptionMap();
-          if (!exceptionMap.containsKey(dataNodeId)
-              || exceptionMap.get(dataNodeId).getTimeStamp() < 
message.getTimeStamp()) {
-            exceptionMap.put(
-                dataNodeId,
-                new PipeRuntimeCriticalException(message.getMessage(), 
message.getTimeStamp()));
-          }
+          recordDataNodePushPipeMetaException(
+              pipeMeta, dataNodeId, message.getMessage(), 
message.getTimeStamp());
         }
       }
     }
@@ -1210,6 +1225,48 @@ public class PipeTaskInfo implements SnapshotProcessor {
     return hasException;
   }
 
+  private void recordUnscopedDataNodePushPipeMetaException(
+      final PipeStaticMeta fallbackPipeStaticMeta,
+      final int dataNodeId,
+      final String message,
+      final long timeStamp) {
+    if (fallbackPipeStaticMeta == null) {
+      pipeMetaKeeper
+          .getPipeMetaList()
+          .forEach(
+              pipeMeta ->
+                  recordDataNodePushPipeMetaException(pipeMeta, dataNodeId, 
message, timeStamp));
+    } else {
+      final PipeMeta pipeMeta = 
pipeMetaKeeper.getPipeMeta(fallbackPipeStaticMeta);
+      if (pipeMeta != null) {
+        recordDataNodePushPipeMetaException(pipeMeta, dataNodeId, message, 
timeStamp);
+      }
+    }
+  }
+
+  private void recordDataNodePushPipeMetaException(
+      final PipeMeta pipeMeta, final int dataNodeId, final String message, 
final long timeStamp) {
+    final PipeRuntimeMeta runtimeMeta = pipeMeta.getRuntimeMeta();
+
+    if (PipeStatus.PRE_DELETE.equals(runtimeMeta.getStatus().get())) {
+      return;
+    }
+
+    // Keep user-stopped pipes out of the auto-restart flow, but retain the 
failure for SHOW PIPES.
+    if (!PipeStatus.STOPPED.equals(runtimeMeta.getStatus().get())
+        || runtimeMeta.getIsStoppedByRuntimeException()) {
+      runtimeMeta.getStatus().set(PipeStatus.STOPPED);
+      runtimeMeta.setIsStoppedByRuntimeException(true);
+    }
+
+    final Map<Integer, PipeRuntimeException> exceptionMap =
+        runtimeMeta.getNodeId2PipeRuntimeExceptionMap();
+    if (!exceptionMap.containsKey(dataNodeId)
+        || exceptionMap.get(dataNodeId).getTimeStamp() < timeStamp) {
+      exceptionMap.put(dataNodeId, new PipeRuntimeCriticalException(message, 
timeStamp));
+    }
+  }
+
   public boolean autoRestart() {
     acquireWriteLock();
     try {
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 0710fe345b8..c52864f4328 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
@@ -21,9 +21,12 @@ package org.apache.iotdb.confignode.procedure.impl.pipe;
 
 import org.apache.iotdb.common.rpc.thrift.TConsensusGroupId;
 import org.apache.iotdb.common.rpc.thrift.TConsensusGroupType;
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
 import org.apache.iotdb.commons.pipe.agent.task.meta.PipeMeta;
 import org.apache.iotdb.commons.pipe.agent.task.meta.PipeStaticMeta;
 import org.apache.iotdb.commons.pipe.config.constant.SystemConstant;
+import 
org.apache.iotdb.confignode.consensus.request.write.pipe.runtime.PipeHandleMetaChangePlan;
+import org.apache.iotdb.confignode.i18n.ConfigNodeMessages;
 import org.apache.iotdb.confignode.i18n.ProcedureMessages;
 import 
org.apache.iotdb.confignode.manager.pipe.metric.overview.PipeProcedureMetrics;
 import org.apache.iotdb.confignode.persistence.pipe.PipeTaskInfo;
@@ -36,6 +39,7 @@ import 
org.apache.iotdb.confignode.procedure.state.ProcedureLockState;
 import 
org.apache.iotdb.confignode.procedure.state.pipe.task.OperatePipeTaskState;
 import org.apache.iotdb.confignode.rpc.thrift.TDatabaseSchema;
 import org.apache.iotdb.confignode.service.ConfigNode;
+import org.apache.iotdb.consensus.exception.ConsensusException;
 import org.apache.iotdb.db.pipe.source.dataregion.DataRegionListeningFilter;
 import org.apache.iotdb.mpp.rpc.thrift.TPushPipeMetaResp;
 import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameters;
@@ -655,14 +659,16 @@ public abstract class AbstractOperatePipeProcedureV2
             .forEach(
                 message -> {
                   // Ignore the timeStamp for simplicity
-                  if (pipeName == null) {
-                    enoughMemoryMessageBuilder.append(
-                        String.format(
-                            "PipeName: %s, Message: %s",
-                            message.getPipeName(), message.getMessage()));
-                  } else if (pipeName.equals(message.getPipeName())) {
-                    enoughMemoryMessageBuilder.append(
-                        String.format("Message: %s", message.getMessage()));
+                  if (isPipeMetaExceptionForPipe(pipeName, 
message.getPipeName())) {
+                    if (pipeName == null) {
+                      enoughMemoryMessageBuilder.append(
+                          String.format(
+                              "PipeName: %s, Message: %s",
+                              message.getPipeName(), message.getMessage()));
+                    } else {
+                      enoughMemoryMessageBuilder.append(
+                          String.format("Message: %s", message.getMessage()));
+                    }
                   }
                 });
         enoughMemoryMessageBuilder.append(".");
@@ -678,7 +684,7 @@ public abstract class AbstractOperatePipeProcedureV2
       }
 
       if (resp.getStatus().getCode() == 
TSStatusCode.PIPE_PUSH_META_ERROR.getStatusCode()) {
-        if (!resp.isSetExceptionMessages()) {
+        if (!resp.isSetExceptionMessages() || 
resp.getExceptionMessages().isEmpty()) {
           final String statusMessage = resp.getStatus().getMessage();
           exceptionMessageBuilder.append(
               String.format(
@@ -693,16 +699,17 @@ public abstract class AbstractOperatePipeProcedureV2
             .forEach(
                 message -> {
                   // Ignore the timeStamp for simplicity
-                  if (pipeName == null) {
+                  if (isPipeMetaExceptionForPipe(pipeName, 
message.getPipeName())) {
                     hasException.set(true);
-                    exceptionMessageBuilder.append(
-                        String.format(
-                            "PipeName: %s, Message: %s",
-                            message.getPipeName(), message.getMessage()));
-                  } else if (pipeName.equals(message.getPipeName())) {
-                    hasException.set(true);
-                    exceptionMessageBuilder.append(
-                        String.format("Message: %s", message.getMessage()));
+                    if (pipeName == null) {
+                      exceptionMessageBuilder.append(
+                          String.format(
+                              "PipeName: %s, Message: %s",
+                              message.getPipeName(), message.getMessage()));
+                    } else {
+                      exceptionMessageBuilder.append(
+                          String.format("Message: %s", message.getMessage()));
+                    }
                   }
                 });
 
@@ -722,6 +729,14 @@ public abstract class AbstractOperatePipeProcedureV2
     return exceptionMessageBuilder.toString();
   }
 
+  private static boolean isPipeMetaExceptionForPipe(
+      final String pipeName, final String exceptionPipeName) {
+    return pipeName == null
+        || exceptionPipeName == null
+        || exceptionPipeName.isEmpty()
+        || pipeName.equals(exceptionPipeName);
+  }
+
   protected void pushPipeMetaToDataNodesIgnoreException(ConfigNodeProcedureEnv 
env) {
     try {
       // Ignore the exceptions reported
@@ -741,6 +756,35 @@ public abstract class AbstractOperatePipeProcedureV2
         pipeMetaBinaryList, this::setPendingDataNodeIds);
   }
 
+  protected void writePipeMetaChangesToConfigNodeConsensus(final 
ConfigNodeProcedureEnv env) {
+    final List<PipeMeta> pipeMetaList = new ArrayList<>();
+    for (final PipeMeta pipeMeta : pipeTaskInfo.get().getPipeMetaList()) {
+      pipeMetaList.add(pipeMeta);
+    }
+
+    TSStatus response;
+    try {
+      response =
+          env.getConfigManager()
+              .getConsensusManager()
+              .write(new PipeHandleMetaChangePlan(pipeMetaList));
+    } catch (final ConsensusException e) {
+      
LOGGER.warn(ConfigNodeMessages.FAILED_IN_THE_WRITE_API_EXECUTING_THE_CONSENSUS_LAYER_DUE,
 e);
+      response = new 
TSStatus(TSStatusCode.EXECUTE_STATEMENT_ERROR.getStatusCode());
+      response.setMessage(e.getMessage());
+    }
+    if (response.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+      throw new PipeException(response.getMessage());
+    }
+    pipeTaskInfo.get().markPipeMetaExceptionsPersisted();
+  }
+
+  protected void persistPendingPipeMetaExceptions(final ConfigNodeProcedureEnv 
env) {
+    if (pipeTaskInfo.get().hasPendingPipeMetaExceptionPersistence()) {
+      writePipeMetaChangesToConfigNodeConsensus(env);
+    }
+  }
+
   protected void pushPipeMetaToDataNodesBestEffort(ConfigNodeProcedureEnv env) 
{
     try {
       pushPipeMetaToDataNodesBestEffortAndGetResponse(env);
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 f18cfb6d545..0c578cf1b6b 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
@@ -204,6 +204,7 @@ public class PipeMetaSyncProcedure extends 
AbstractOperatePipeProcedureV2 {
     if (response.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
       throw new PipeException(response.getMessage());
     }
+    pipeTaskInfo.get().markPipeMetaExceptionsPersisted();
   }
 
   @Override
@@ -211,8 +212,11 @@ public class PipeMetaSyncProcedure extends 
AbstractOperatePipeProcedureV2 {
       throws PipeException, IOException {
     
LOGGER.debug(ProcedureMessages.PIPEMETASYNCPROCEDURE_EXECUTEFROMOPERATEONDATANODES);
 
+    persistPendingPipeMetaExceptions(env);
+
     Map<Integer, TPushPipeMetaResp> respMap = 
pushPipeMetaToDataNodesBestEffortAndGetResponse(env);
     if (pipeTaskInfo.get().recordDataNodePushPipeMetaExceptions(respMap)) {
+      writePipeMetaChangesToConfigNodeConsensus(env);
       throw new PipeException(
           String.format(
               ProcedureMessages.FAILED_TO_PUSH_PIPE_META_TO_DATANODES_DETAILS,
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 f9c3ec22946..cf380ac0853 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
@@ -377,8 +377,13 @@ public class AlterPipeProcedureV2 extends 
AbstractOperatePipeProcedureV2 {
     final String pipeName = alterPipeRequest.getPipeName();
     
LOGGER.info(ProcedureMessages.ALTERPIPEPROCEDUREV2_EXECUTEFROMOPERATEONDATANODES,
 pipeName);
 
-    final String exceptionMessage =
-        parsePushPipeMetaExceptionForPipe(pipeName, 
pushAlteredPipeMetaToDataNodes(env));
+    persistPendingPipeMetaExceptions(env);
+
+    final Map<Integer, TPushPipeMetaResp> respMap = 
pushAlteredPipeMetaToDataNodes(env);
+    if (pipeTaskInfo.get().recordDataNodePushPipeMetaExceptions(respMap, 
updatedPipeStaticMeta)) {
+      writePipeMetaChangesToConfigNodeConsensus(env);
+    }
+    final String exceptionMessage = 
parsePushPipeMetaExceptionForPipe(pipeName, respMap);
     if (!exceptionMessage.isEmpty()) {
       LOGGER.warn(
           
ProcedureMessages.FAILED_TO_ALTER_PIPE_DETAILS_METADATA_WILL_BE_SYNCHRONIZED_LATER,
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 f62402db84e..40134cf6dd9 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
@@ -51,6 +51,7 @@ import 
org.apache.iotdb.confignode.procedure.store.ProcedureType;
 import org.apache.iotdb.confignode.rpc.thrift.TCreatePipeReq;
 import org.apache.iotdb.confignode.rpc.thrift.TPermissionInfoResp;
 import org.apache.iotdb.consensus.exception.ConsensusException;
+import org.apache.iotdb.mpp.rpc.thrift.TPushPipeMetaResp;
 import org.apache.iotdb.pipe.api.PipePlugin;
 import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameters;
 import org.apache.iotdb.pipe.api.exception.PipeException;
@@ -411,9 +412,14 @@ public class CreatePipeProcedureV2 extends 
AbstractOperatePipeProcedureV2 {
     final String pipeName = createPipeRequest.getPipeName();
     
LOGGER.info(ProcedureMessages.CREATEPIPEPROCEDUREV2_EXECUTEFROMOPERATEONDATANODES,
 pipeName);
 
-    final String exceptionMessage =
-        parsePushPipeMetaExceptionForPipe(
-            pipeName, pushSinglePipeMetaToDataNodes(pipeStaticMeta, env));
+    persistPendingPipeMetaExceptions(env);
+
+    final Map<Integer, TPushPipeMetaResp> respMap =
+        pushSinglePipeMetaToDataNodes(pipeStaticMeta, env);
+    if (pipeTaskInfo.get().recordDataNodePushPipeMetaExceptions(respMap, 
pipeStaticMeta)) {
+      writePipeMetaChangesToConfigNodeConsensus(env);
+    }
+    final String exceptionMessage = 
parsePushPipeMetaExceptionForPipe(pipeName, respMap);
     if (!exceptionMessage.isEmpty()) {
       LOGGER.warn(
           
ProcedureMessages.FAILED_TO_CREATE_PIPE_DETAILS_METADATA_WILL_BE_SYNCHRONIZED_LATER,
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/task/StartPipeProcedureV2.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/task/StartPipeProcedureV2.java
index a3d0fb01d69..31be648dcbd 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/task/StartPipeProcedureV2.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/pipe/task/StartPipeProcedureV2.java
@@ -23,7 +23,6 @@ import org.apache.iotdb.common.rpc.thrift.TSStatus;
 import org.apache.iotdb.commons.pipe.agent.task.meta.PipeMeta;
 import org.apache.iotdb.commons.pipe.agent.task.meta.PipeStaticMeta;
 import org.apache.iotdb.commons.pipe.agent.task.meta.PipeStatus;
-import 
org.apache.iotdb.confignode.consensus.request.write.pipe.runtime.PipeHandleMetaChangePlan;
 import 
org.apache.iotdb.confignode.consensus.request.write.pipe.task.SetPipeStatusPlanV2;
 import org.apache.iotdb.confignode.i18n.ConfigNodeMessages;
 import org.apache.iotdb.confignode.i18n.ProcedureMessages;
@@ -32,6 +31,7 @@ import 
org.apache.iotdb.confignode.procedure.impl.pipe.AbstractOperatePipeProced
 import org.apache.iotdb.confignode.procedure.impl.pipe.PipeTaskOperation;
 import org.apache.iotdb.confignode.procedure.store.ProcedureType;
 import org.apache.iotdb.consensus.exception.ConsensusException;
+import org.apache.iotdb.mpp.rpc.thrift.TPushPipeMetaResp;
 import org.apache.iotdb.pipe.api.exception.PipeException;
 import org.apache.iotdb.rpc.TSStatusCode;
 
@@ -42,8 +42,7 @@ import org.slf4j.LoggerFactory;
 import java.io.DataOutputStream;
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 
 public class StartPipeProcedureV2 extends AbstractOperatePipeProcedureV2 {
@@ -129,14 +128,30 @@ public class StartPipeProcedureV2 extends 
AbstractOperatePipeProcedureV2 {
         isTableModelSet
             ? pipeTaskInfo.get().isStoppedByRuntimeException(pipeName, 
isTableModel)
             : pipeTaskInfo.get().isStoppedByRuntimeException(pipeName);
-    final PipeStaticMeta pipeStaticMeta =
-        (isTableModelSet
-                ? pipeTaskInfo.get().getPipeMetaByPipeName(pipeName, 
isTableModel)
-                : pipeTaskInfo.get().getPipeMetaByPipeName(pipeName))
-            .getStaticMeta();
-    final String exceptionMessage =
-        parsePushPipeMetaExceptionForPipe(
-            pipeName, pushSinglePipeMetaToDataNodes(pipeStaticMeta, env));
+    final boolean hadPendingPipeMetaExceptions =
+        pipeTaskInfo.get().hasPendingPipeMetaExceptionPersistence();
+    if (hadPendingPipeMetaExceptions) {
+      persistPendingPipeMetaExceptions(env);
+    }
+    final PipeMeta pipeMeta =
+        isTableModelSet
+            ? pipeTaskInfo.get().getPipeMetaByPipeName(pipeName, isTableModel)
+            : pipeTaskInfo.get().getPipeMetaByPipeName(pipeName);
+    // A previous attempt may already have recorded a runtime stop before a 
failed consensus write
+    // or procedure recovery. Persist that state without treating a STOPPED 
meta push as a start.
+    if (pipeMeta.getRuntimeMeta().getStatus().get() != PipeStatus.RUNNING) {
+      if (!hadPendingPipeMetaExceptions) {
+        writePipeMetaChangesToConfigNodeConsensus(env);
+      }
+      return;
+    }
+    final PipeStaticMeta pipeStaticMeta = pipeMeta.getStaticMeta();
+    final Map<Integer, TPushPipeMetaResp> respMap =
+        pushSinglePipeMetaToDataNodes(pipeStaticMeta, env);
+    if (pipeTaskInfo.get().recordDataNodePushPipeMetaExceptions(respMap, 
pipeStaticMeta)) {
+      writePipeMetaChangesToConfigNodeConsensus(env);
+    }
+    final String exceptionMessage = 
parsePushPipeMetaExceptionForPipe(pipeName, respMap);
     if (!exceptionMessage.isEmpty()) {
       LOGGER.warn(
           
ProcedureMessages.FAILED_TO_START_PIPE_DETAILS_METADATA_WILL_BE_SYNCHRONIZED_LATER,
@@ -163,28 +178,6 @@ public class StartPipeProcedureV2 extends 
AbstractOperatePipeProcedureV2 {
     }
   }
 
-  private void writePipeMetaChangesToConfigNodeConsensus(final 
ConfigNodeProcedureEnv env) {
-    final List<PipeMeta> pipeMetaList = new ArrayList<>();
-    for (final PipeMeta pipeMeta : pipeTaskInfo.get().getPipeMetaList()) {
-      pipeMetaList.add(pipeMeta);
-    }
-
-    TSStatus response;
-    try {
-      response =
-          env.getConfigManager()
-              .getConsensusManager()
-              .write(new PipeHandleMetaChangePlan(pipeMetaList));
-    } catch (ConsensusException e) {
-      
LOGGER.warn(ConfigNodeMessages.FAILED_IN_THE_WRITE_API_EXECUTING_THE_CONSENSUS_LAYER_DUE,
 e);
-      response = new 
TSStatus(TSStatusCode.EXECUTE_STATEMENT_ERROR.getStatusCode());
-      response.setMessage(e.getMessage());
-    }
-    if (response.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
-      throw new PipeException(response.getMessage());
-    }
-  }
-
   @Override
   public void rollbackFromValidateTask(ConfigNodeProcedureEnv env) {
     
LOGGER.info(ProcedureMessages.STARTPIPEPROCEDUREV2_ROLLBACKFROMVALIDATETASK, 
pipeName);
diff --git 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/consensus/response/pipe/PipeTableRespTest.java
 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/consensus/response/pipe/PipeTableRespTest.java
index b3ae595a5ca..66bc79f3cea 100644
--- 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/consensus/response/pipe/PipeTableRespTest.java
+++ 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/consensus/response/pipe/PipeTableRespTest.java
@@ -20,6 +20,7 @@ package org.apache.iotdb.confignode.consensus.response.pipe;
 
 import org.apache.iotdb.common.rpc.thrift.TSStatus;
 import org.apache.iotdb.commons.consensus.index.impl.MinimumProgressIndex;
+import org.apache.iotdb.commons.exception.pipe.PipeRuntimeCriticalException;
 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;
@@ -199,6 +200,25 @@ public class PipeTableRespTest {
     Assert.assertEquals(PipeStatus.PRE_DELETE.name(), 
showPipeResult.get(0).getState());
   }
 
+  @Test
+  public void 
testConvertToTShowPipeRespIncludesStoppedStateAndExceptionMessage() {
+    final PipeTableResp pipeTableResp = constructPipeTableResp();
+    final PipeMeta pipeMeta = pipeTableResp.getAllPipeMeta().get(0);
+    pipeMeta.getRuntimeMeta().getStatus().set(PipeStatus.STOPPED);
+    pipeMeta
+        .getRuntimeMeta()
+        .getNodeId2PipeRuntimeExceptionMap()
+        .put(
+            1, new PipeRuntimeCriticalException("construction failed", 
System.currentTimeMillis()));
+
+    final TShowPipeInfo showPipeInfo =
+        pipeTableResp.convertToTShowPipeResp().getPipeInfoList().get(0);
+
+    Assert.assertEquals(PipeStatus.STOPPED.name(), showPipeInfo.getState());
+    
Assert.assertTrue(showPipeInfo.getExceptionMessage().contains("construction 
failed"));
+    Assert.assertTrue(showPipeInfo.getExceptionMessage().contains("nodeIds: 
[1]"));
+  }
+
   @Test
   public void testFilterByModelBeforeWhereClause() {
     TSStatus status = new 
TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode());
diff --git 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/pipe/PipeTaskInfoAutoRestartTest.java
 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/pipe/PipeTaskInfoAutoRestartTest.java
index fd9540aa7f6..b5e443c891b 100644
--- 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/pipe/PipeTaskInfoAutoRestartTest.java
+++ 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/pipe/PipeTaskInfoAutoRestartTest.java
@@ -34,6 +34,7 @@ import 
org.apache.iotdb.confignode.consensus.request.write.pipe.task.CreatePipeP
 import 
org.apache.iotdb.confignode.consensus.request.write.pipe.task.DropPipePlanV2;
 import 
org.apache.iotdb.confignode.consensus.request.write.pipe.task.SetPipeStatusPlanV2;
 import org.apache.iotdb.confignode.i18n.ConfigNodeMessages;
+import org.apache.iotdb.confignode.i18n.ProcedureMessages;
 import org.apache.iotdb.confignode.rpc.thrift.TAlterPipeReq;
 import org.apache.iotdb.mpp.rpc.thrift.TPushPipeMetaResp;
 import org.apache.iotdb.mpp.rpc.thrift.TPushPipeMetaRespExceptionMessage;
@@ -80,6 +81,58 @@ public class PipeTaskInfoAutoRestartTest {
     Assert.assertEquals(PipeStatus.RUNNING, runtimeMeta.getStatus().get());
   }
 
+  @Test
+  public void 
testRecordDataNodePushPipeMetaExceptionsUsesFallbackForEmptyDetails() {
+    final String pipeName = "emptyDetailsPipe";
+    final PipeStaticMeta staticMeta = createPipe(pipeName, PipeStatus.RUNNING);
+    final TPushPipeMetaResp response =
+        new TPushPipeMetaResp()
+            .setStatus(new 
TSStatus(TSStatusCode.PIPE_PUSH_META_ERROR.getStatusCode()))
+            .setExceptionMessages(Collections.emptyList());
+
+    Assert.assertTrue(
+        pipeTaskInfo.recordDataNodePushPipeMetaExceptions(
+            Collections.singletonMap(DATA_NODE_ID, response), staticMeta));
+
+    final PipeRuntimeMeta runtimeMeta =
+        pipeTaskInfo.getPipeMetaByPipeName(pipeName).getRuntimeMeta();
+    Assert.assertEquals(PipeStatus.STOPPED, runtimeMeta.getStatus().get());
+    Assert.assertTrue(runtimeMeta.getIsStoppedByRuntimeException());
+    Assert.assertTrue(
+        runtimeMeta
+            .getNodeId2PipeRuntimeExceptionMap()
+            .get(DATA_NODE_ID)
+            .getMessage()
+            .contains(
+                ProcedureMessages
+                    
.EXCEPTION_INTERNAL_ERROR_WHILE_PROCESSING_PUSHPIPEMETA_ON_DATANODE_5983BD5F));
+  }
+
+  @Test
+  public void 
testRecordDataNodePushPipeMetaExceptionsUsesFallbackForEmptyPipeName() {
+    final String pipeName = "emptyPipeNamePipe";
+    final PipeStaticMeta staticMeta = createPipe(pipeName, PipeStatus.RUNNING);
+    final TPushPipeMetaRespExceptionMessage exceptionMessage =
+        new TPushPipeMetaRespExceptionMessage(
+            "", "failed to close schema listening queue", 
System.currentTimeMillis());
+    final TPushPipeMetaResp response =
+        new TPushPipeMetaResp()
+            .setStatus(new 
TSStatus(TSStatusCode.PIPE_PUSH_META_ERROR.getStatusCode()))
+            .setExceptionMessages(Collections.singletonList(exceptionMessage));
+
+    Assert.assertTrue(
+        pipeTaskInfo.recordDataNodePushPipeMetaExceptions(
+            Collections.singletonMap(DATA_NODE_ID, response), staticMeta));
+
+    final PipeRuntimeMeta runtimeMeta =
+        pipeTaskInfo.getPipeMetaByPipeName(pipeName).getRuntimeMeta();
+    Assert.assertEquals(PipeStatus.STOPPED, runtimeMeta.getStatus().get());
+    Assert.assertTrue(runtimeMeta.getIsStoppedByRuntimeException());
+    Assert.assertEquals(
+        "failed to close schema listening queue",
+        
runtimeMeta.getNodeId2PipeRuntimeExceptionMap().get(DATA_NODE_ID).getMessage());
+  }
+
   @Test
   public void 
testRecordDataNodePushPipeMetaExceptionsKeepsUserStoppedPipeOutOfAutoRestart() {
     final String pipeName = "stoppedPipe";
@@ -94,6 +147,7 @@ public class PipeTaskInfoAutoRestartTest {
     Assert.assertEquals(PipeStatus.STOPPED, runtimeMeta.getStatus().get());
     Assert.assertFalse(runtimeMeta.getIsStoppedByRuntimeException());
     Assert.assertTrue(pipeTaskInfo.isPipeStoppedByUser(pipeName));
+    
Assert.assertTrue(runtimeMeta.getNodeId2PipeRuntimeExceptionMap().containsKey(DATA_NODE_ID));
 
     Assert.assertFalse(pipeTaskInfo.autoRestart());
     Assert.assertEquals(PipeStatus.STOPPED, runtimeMeta.getStatus().get());
diff --git 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/pipe/AbstractOperatePipeProcedureV2Test.java
 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/pipe/AbstractOperatePipeProcedureV2Test.java
index ae0feec0f72..a4a75d60dc7 100644
--- 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/pipe/AbstractOperatePipeProcedureV2Test.java
+++ 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/pipe/AbstractOperatePipeProcedureV2Test.java
@@ -19,21 +19,45 @@
 
 package org.apache.iotdb.confignode.procedure.impl.pipe;
 
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
 import org.apache.iotdb.confignode.persistence.pipe.PipeTaskInfo;
 import org.apache.iotdb.confignode.procedure.Procedure;
 import org.apache.iotdb.confignode.procedure.impl.StateMachineProcedure;
 import 
org.apache.iotdb.confignode.procedure.state.pipe.task.OperatePipeTaskState;
+import org.apache.iotdb.mpp.rpc.thrift.TPushPipeMetaResp;
+import org.apache.iotdb.mpp.rpc.thrift.TPushPipeMetaRespExceptionMessage;
 import org.apache.iotdb.pipe.api.exception.PipeException;
+import org.apache.iotdb.rpc.TSStatusCode;
 
 import org.junit.Assert;
 import org.junit.Test;
 
 import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
 
 public class AbstractOperatePipeProcedureV2Test {
 
+  @Test
+  public void testParsePushPipeMetaExceptionForPipeIncludesUnscopedException() 
{
+    final TPushPipeMetaRespExceptionMessage exceptionMessage =
+        new TPushPipeMetaRespExceptionMessage(
+            "", "failed to close schema listening queue", 
System.currentTimeMillis());
+    final TPushPipeMetaResp response =
+        new TPushPipeMetaResp()
+            .setStatus(new 
TSStatus(TSStatusCode.PIPE_PUSH_META_ERROR.getStatusCode()))
+            .setExceptionMessages(Collections.singletonList(exceptionMessage));
+
+    final String message =
+        AbstractOperatePipeProcedureV2.parsePushPipeMetaExceptionForPipe(
+            "targetPipe", Map.of(1, response));
+
+    Assert.assertTrue(message.contains("DataNodeId: 1"));
+    Assert.assertTrue(message.contains("Message: failed to close schema 
listening queue"));
+  }
+
   @Test
   public void testSuccessfulStateDoesNotYield() throws Exception {
     final TestOperatePipeProcedure procedure = new TestOperatePipeProcedure();
diff --git 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/pipe/runtime/PipeMetaSyncProcedureTest.java
 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/pipe/runtime/PipeMetaSyncProcedureTest.java
index 44b96284a15..9b41b7e8dbb 100644
--- 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/pipe/runtime/PipeMetaSyncProcedureTest.java
+++ 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/pipe/runtime/PipeMetaSyncProcedureTest.java
@@ -19,15 +19,42 @@
 
 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.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;
+import org.apache.iotdb.confignode.consensus.request.ConfigPhysicalPlan;
+import 
org.apache.iotdb.confignode.consensus.request.write.pipe.runtime.PipeHandleMetaChangePlan;
+import 
org.apache.iotdb.confignode.consensus.request.write.pipe.task.CreatePipePlanV2;
+import org.apache.iotdb.confignode.manager.ConfigManager;
+import org.apache.iotdb.confignode.manager.consensus.ConsensusManager;
+import org.apache.iotdb.confignode.persistence.pipe.PipeTaskInfo;
+import org.apache.iotdb.confignode.procedure.env.ConfigNodeProcedureEnv;
 import org.apache.iotdb.confignode.procedure.store.ProcedureFactory;
+import org.apache.iotdb.mpp.rpc.thrift.TPushPipeMetaResp;
+import org.apache.iotdb.mpp.rpc.thrift.TPushPipeMetaRespExceptionMessage;
+import org.apache.iotdb.pipe.api.exception.PipeException;
+import org.apache.iotdb.rpc.TSStatusCode;
 
 import org.apache.tsfile.utils.PublicBAOS;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
 
 import java.io.DataOutputStream;
+import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicReference;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 public class PipeMetaSyncProcedureTest {
@@ -52,4 +79,217 @@ public class PipeMetaSyncProcedureTest {
       fail();
     }
   }
+
+  @Test
+  public void testDataNodePushFailureIsPersistedToConsensus() throws Exception 
{
+    final String pipeName = "metaSyncFailurePipe";
+    final long creationTime = System.currentTimeMillis();
+    final PipeStaticMeta staticMeta =
+        new PipeStaticMeta(
+            pipeName,
+            creationTime,
+            Collections.singletonMap("extractor", "iotdb-source"),
+            Collections.singletonMap("processor", "do-nothing-processor"),
+            Collections.singletonMap("connector", "write-back-sink"));
+    final PipeRuntimeMeta runtimeMeta = new PipeRuntimeMeta(new 
ConcurrentHashMap<>());
+    runtimeMeta.getStatus().set(PipeStatus.RUNNING);
+    runtimeMeta
+        .getConsensusGroupId2TaskMetaMap()
+        .put(1, new PipeTaskMeta(MinimumProgressIndex.INSTANCE, 1));
+    final PipeTaskInfo taskInfo = new PipeTaskInfo();
+    taskInfo.createPipe(new CreatePipePlanV2(staticMeta, runtimeMeta));
+
+    final String error = "Failed to construct PipeConnector";
+    final TPushPipeMetaRespExceptionMessage exceptionMessage =
+        new TPushPipeMetaRespExceptionMessage(pipeName, error, creationTime)
+            .setCreationTime(creationTime);
+    final Map<Integer, TPushPipeMetaResp> responses =
+        Collections.singletonMap(
+            1,
+            new TPushPipeMetaResp(new 
TSStatus(TSStatusCode.PIPE_PUSH_META_ERROR.getStatusCode()))
+                
.setExceptionMessages(Collections.singletonList(exceptionMessage)));
+
+    final ConfigNodeProcedureEnv env = 
Mockito.mock(ConfigNodeProcedureEnv.class);
+    final ConfigManager configManager = Mockito.mock(ConfigManager.class);
+    final ConsensusManager consensusManager = 
Mockito.mock(ConsensusManager.class);
+    Mockito.when(env.getConfigManager()).thenReturn(configManager);
+    
Mockito.when(configManager.getConsensusManager()).thenReturn(consensusManager);
+    Mockito.when(consensusManager.write(Mockito.any(ConfigPhysicalPlan.class)))
+        .thenReturn(new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()));
+
+    final PipeMetaSyncProcedure procedure = new 
TestablePipeMetaSyncProcedure(taskInfo, responses);
+    try {
+      procedure.executeFromOperateOnDataNodes(env);
+      fail("Expected the sync procedure to report the DataNode push failure");
+    } catch (final PipeException expected) {
+      assertTrue(expected.getMessage().contains(error));
+    }
+
+    final ArgumentCaptor<ConfigPhysicalPlan> planCaptor =
+        ArgumentCaptor.forClass(ConfigPhysicalPlan.class);
+    Mockito.verify(consensusManager).write(planCaptor.capture());
+    assertTrue(planCaptor.getValue() instanceof PipeHandleMetaChangePlan);
+    final PipeMeta persistedPipeMeta =
+        ((PipeHandleMetaChangePlan) planCaptor.getValue())
+            .getPipeMetaList().stream()
+                .filter(meta -> 
pipeName.equals(meta.getStaticMeta().getPipeName()))
+                .findFirst()
+                .orElseThrow(AssertionError::new);
+    assertEquals(PipeStatus.STOPPED, 
persistedPipeMeta.getRuntimeMeta().getStatus().get());
+    
assertTrue(persistedPipeMeta.getRuntimeMeta().getIsStoppedByRuntimeException());
+    assertEquals(
+        error,
+        
persistedPipeMeta.getRuntimeMeta().getNodeId2PipeRuntimeExceptionMap().get(1).getMessage());
+  }
+
+  @Test
+  public void testUnscopedPushFailureStopsAndPersistsAllPipes() throws 
Exception {
+    final PipeTaskInfo taskInfo = new PipeTaskInfo();
+    final PipeStaticMeta firstPipe = createRunningPipe(taskInfo, 
"firstUnscopedPipe");
+    final PipeStaticMeta secondPipe = createRunningPipe(taskInfo, 
"secondUnscopedPipe");
+    final String statusMessage = "DataNode failed before returning 
pipe-specific details";
+    final Map<Integer, TPushPipeMetaResp> responses =
+        Collections.singletonMap(
+            1,
+            new TPushPipeMetaResp(
+                new TSStatus(TSStatusCode.PIPE_PUSH_META_ERROR.getStatusCode())
+                    .setMessage(statusMessage)));
+
+    final ConfigNodeProcedureEnv env = 
Mockito.mock(ConfigNodeProcedureEnv.class);
+    final ConfigManager configManager = Mockito.mock(ConfigManager.class);
+    final ConsensusManager consensusManager = 
Mockito.mock(ConsensusManager.class);
+    Mockito.when(env.getConfigManager()).thenReturn(configManager);
+    
Mockito.when(configManager.getConsensusManager()).thenReturn(consensusManager);
+    Mockito.when(consensusManager.write(Mockito.any(ConfigPhysicalPlan.class)))
+        .thenReturn(new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()));
+
+    final PipeMetaSyncProcedure procedure = new 
TestablePipeMetaSyncProcedure(taskInfo, responses);
+    try {
+      procedure.executeFromOperateOnDataNodes(env);
+      fail("Expected the sync procedure to report the DataNode push failure");
+    } catch (final PipeException expected) {
+      assertTrue(expected.getMessage().contains(statusMessage));
+    }
+
+    final ArgumentCaptor<ConfigPhysicalPlan> planCaptor =
+        ArgumentCaptor.forClass(ConfigPhysicalPlan.class);
+    Mockito.verify(consensusManager).write(planCaptor.capture());
+    assertTrue(planCaptor.getValue() instanceof PipeHandleMetaChangePlan);
+    final PipeHandleMetaChangePlan persistedPlan =
+        (PipeHandleMetaChangePlan)
+            
ConfigPhysicalPlan.Factory.create(planCaptor.getValue().serializeToByteBuffer());
+    final Map<String, PipeMeta> persistedPipes =
+        persistedPlan.getPipeMetaList().stream()
+            .collect(
+                java.util.stream.Collectors.toMap(
+                    meta -> meta.getStaticMeta().getPipeName(), meta -> meta));
+    assertEquals(2, persistedPipes.size());
+    for (final PipeStaticMeta pipeStaticMeta : new PipeStaticMeta[] 
{firstPipe, secondPipe}) {
+      final PipeRuntimeMeta persistedRuntimeMeta =
+          persistedPipes.get(pipeStaticMeta.getPipeName()).getRuntimeMeta();
+      assertEquals(PipeStatus.STOPPED, persistedRuntimeMeta.getStatus().get());
+      assertTrue(persistedRuntimeMeta.getIsStoppedByRuntimeException());
+      assertTrue(
+          persistedRuntimeMeta
+              .getNodeId2PipeRuntimeExceptionMap()
+              .get(1)
+              .getMessage()
+              .contains(statusMessage));
+    }
+    assertTrue(
+        taskInfo
+            .getPipeMetaByPipeName(firstPipe.getPipeName())
+            .getRuntimeMeta()
+            .getNodeId2PipeRuntimeExceptionMap()
+            .containsKey(1));
+  }
+
+  @Test
+  public void testRetriesPersistenceWhenPushSucceedsAfterConsensusFailure() 
throws Exception {
+    final PipeTaskInfo taskInfo = new PipeTaskInfo();
+    final PipeStaticMeta pipeStaticMeta = createRunningPipe(taskInfo, 
"metaSyncRetryPipe");
+    final String error = "DataNode failed before returning pipe-specific 
details";
+    final Map<Integer, TPushPipeMetaResp> responses = new HashMap<>();
+    responses.put(
+        1,
+        new TPushPipeMetaResp(
+            new 
TSStatus(TSStatusCode.PIPE_PUSH_META_ERROR.getStatusCode()).setMessage(error)));
+
+    final ConfigNodeProcedureEnv env = 
Mockito.mock(ConfigNodeProcedureEnv.class);
+    final ConfigManager configManager = Mockito.mock(ConfigManager.class);
+    final ConsensusManager consensusManager = 
Mockito.mock(ConsensusManager.class);
+    Mockito.when(env.getConfigManager()).thenReturn(configManager);
+    
Mockito.when(configManager.getConsensusManager()).thenReturn(consensusManager);
+    Mockito.when(consensusManager.write(Mockito.any(ConfigPhysicalPlan.class)))
+        .thenReturn(new 
TSStatus(TSStatusCode.EXECUTE_STATEMENT_ERROR.getStatusCode()))
+        .thenReturn(new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()));
+
+    final PipeMetaSyncProcedure procedure = new 
TestablePipeMetaSyncProcedure(taskInfo, responses);
+    try {
+      procedure.executeFromOperateOnDataNodes(env);
+      fail("Expected the first attempt to fail its ConfigNode consensus 
write");
+    } catch (final PipeException expected) {
+      // The procedure retries this state after the persistence failure.
+    }
+
+    responses.put(
+        1, new TPushPipeMetaResp(new 
TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode())));
+    procedure.executeFromOperateOnDataNodes(env);
+
+    final ArgumentCaptor<ConfigPhysicalPlan> planCaptor =
+        ArgumentCaptor.forClass(ConfigPhysicalPlan.class);
+    Mockito.verify(consensusManager, 
Mockito.times(2)).write(planCaptor.capture());
+    assertTrue(planCaptor.getAllValues().get(1) instanceof 
PipeHandleMetaChangePlan);
+    final PipeMeta persistedPipeMeta =
+        ((PipeHandleMetaChangePlan) planCaptor.getAllValues().get(1))
+            .getPipeMetaList().stream()
+                .filter(
+                    meta -> 
pipeStaticMeta.getPipeName().equals(meta.getStaticMeta().getPipeName()))
+                .findFirst()
+                .orElseThrow(AssertionError::new);
+    assertEquals(PipeStatus.STOPPED, 
persistedPipeMeta.getRuntimeMeta().getStatus().get());
+    
assertTrue(persistedPipeMeta.getRuntimeMeta().getIsStoppedByRuntimeException());
+    assertTrue(
+        persistedPipeMeta
+            .getRuntimeMeta()
+            .getNodeId2PipeRuntimeExceptionMap()
+            .get(1)
+            .getMessage()
+            .contains(error));
+  }
+
+  private static PipeStaticMeta createRunningPipe(
+      final PipeTaskInfo taskInfo, final String pipeName) {
+    final long creationTime = System.nanoTime();
+    final PipeStaticMeta staticMeta =
+        new PipeStaticMeta(
+            pipeName,
+            creationTime,
+            Collections.singletonMap("extractor", "iotdb-source"),
+            Collections.singletonMap("processor", "do-nothing-processor"),
+            Collections.singletonMap("connector", "write-back-sink"));
+    final PipeRuntimeMeta runtimeMeta = new PipeRuntimeMeta(new 
ConcurrentHashMap<>());
+    runtimeMeta.getStatus().set(PipeStatus.RUNNING);
+    runtimeMeta
+        .getConsensusGroupId2TaskMetaMap()
+        .put(1, new PipeTaskMeta(MinimumProgressIndex.INSTANCE, 1));
+    taskInfo.createPipe(new CreatePipePlanV2(staticMeta, runtimeMeta));
+    return staticMeta;
+  }
+
+  private static class TestablePipeMetaSyncProcedure extends 
PipeMetaSyncProcedure {
+    private final Map<Integer, TPushPipeMetaResp> responses;
+
+    private TestablePipeMetaSyncProcedure(
+        final PipeTaskInfo taskInfo, final Map<Integer, TPushPipeMetaResp> 
responses) {
+      this.pipeTaskInfo = new AtomicReference<>(taskInfo);
+      this.responses = responses;
+    }
+
+    @Override
+    protected Map<Integer, TPushPipeMetaResp> 
pushPipeMetaToDataNodesBestEffortAndGetResponse(
+        final ConfigNodeProcedureEnv env) throws IOException {
+      return responses;
+    }
+  }
 }
diff --git 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/pipe/task/CreatePipeProcedureV2Test.java
 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/pipe/task/CreatePipeProcedureV2Test.java
index 013376571ec..ac90a2ddec1 100644
--- 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/pipe/task/CreatePipeProcedureV2Test.java
+++ 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/pipe/task/CreatePipeProcedureV2Test.java
@@ -20,28 +20,174 @@
 package org.apache.iotdb.confignode.procedure.impl.pipe.task;
 
 import org.apache.iotdb.common.rpc.thrift.TSStatus;
+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.config.constant.PipeSourceConstant;
+import org.apache.iotdb.confignode.consensus.request.ConfigPhysicalPlan;
+import 
org.apache.iotdb.confignode.consensus.request.write.pipe.runtime.PipeHandleMetaChangePlan;
 import org.apache.iotdb.confignode.manager.ConfigManager;
 import org.apache.iotdb.confignode.manager.PermissionManager;
+import org.apache.iotdb.confignode.manager.consensus.ConsensusManager;
+import org.apache.iotdb.confignode.persistence.pipe.PipeTaskInfo;
 import org.apache.iotdb.confignode.procedure.env.ConfigNodeProcedureEnv;
 import org.apache.iotdb.confignode.procedure.store.ProcedureFactory;
 import org.apache.iotdb.confignode.rpc.thrift.TCreatePipeReq;
 import org.apache.iotdb.confignode.rpc.thrift.TPermissionInfoResp;
+import org.apache.iotdb.mpp.rpc.thrift.TPushPipeMetaResp;
+import org.apache.iotdb.mpp.rpc.thrift.TPushPipeMetaRespExceptionMessage;
 import org.apache.iotdb.rpc.TSStatusCode;
 
 import org.apache.tsfile.utils.PublicBAOS;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mockito;
 
 import java.io.DataOutputStream;
 import java.nio.ByteBuffer;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 public class CreatePipeProcedureV2Test {
+  @Test
+  public void testDataNodeConstructionFailureIsRecorded() throws Exception {
+    final String pipeName = "failedHistoryPipe";
+    final TCreatePipeReq request = createPipeRequest(pipeName);
+    final PipeTaskInfo taskInfo = new PipeTaskInfo();
+    final CreatePipeProcedureV2 procedure =
+        new CreatePipeProcedureV2(request, new AtomicReference<>(taskInfo));
+    
procedure.executeFromCalculateInfoForTask(Mockito.mock(ConfigNodeProcedureEnv.class));
+    taskInfo.createPipe(procedure.constructPlan());
+
+    final String error = "Failed to construct PipeConnector";
+    final TPushPipeMetaRespExceptionMessage exceptionMessage =
+        new TPushPipeMetaRespExceptionMessage(request.getPipeName(), error, 1L)
+            .setCreationTime(procedure.getPipeStaticMeta().getCreationTime());
+    final ConfigNodeProcedureEnv env = 
Mockito.mock(ConfigNodeProcedureEnv.class);
+    final ConsensusManager consensusManager = 
mockSuccessfulConsensusWrite(env);
+    
Mockito.when(env.pushSinglePipeMetaToDataNodes(Mockito.any(ByteBuffer.class), 
Mockito.any()))
+        .thenReturn(
+            Collections.singletonMap(
+                1,
+                new TPushPipeMetaResp(
+                        new 
TSStatus(TSStatusCode.PIPE_PUSH_META_ERROR.getStatusCode()))
+                    
.setExceptionMessages(Collections.singletonList(exceptionMessage))));
+
+    procedure.executeFromOperateOnDataNodes(env);
+
+    final PipeRuntimeMeta runtimeMeta =
+        taskInfo.getPipeMetaByPipeName(request.getPipeName()).getRuntimeMeta();
+    assertEquals(PipeStatus.STOPPED, runtimeMeta.getStatus().get());
+    assertTrue(runtimeMeta.getIsStoppedByRuntimeException());
+    assertEquals(error, 
runtimeMeta.getNodeId2PipeRuntimeExceptionMap().get(1).getMessage());
+    assertPipeMetaChangeWasWritten(consensusManager, request.getPipeName(), 
error);
+  }
+
+  @Test
+  public void testRetriesFailurePersistenceBeforeSuccessfulMetaPush() throws 
Exception {
+    final String pipeName = "failedHistoryPipePersistenceRetry";
+    final TCreatePipeReq request = createPipeRequest(pipeName);
+    final PipeTaskInfo taskInfo = new PipeTaskInfo();
+    final CreatePipeProcedureV2 procedure =
+        new CreatePipeProcedureV2(request, new AtomicReference<>(taskInfo));
+    
procedure.executeFromCalculateInfoForTask(Mockito.mock(ConfigNodeProcedureEnv.class));
+    taskInfo.createPipe(procedure.constructPlan());
+
+    final String error = "DataNode failed before returning pipe-specific 
details";
+    final TPushPipeMetaRespExceptionMessage exceptionMessage =
+        new TPushPipeMetaRespExceptionMessage(request.getPipeName(), error, 1L)
+            .setCreationTime(procedure.getPipeStaticMeta().getCreationTime());
+    final Map<Integer, TPushPipeMetaResp> responses = new HashMap<>();
+    responses.put(
+        1,
+        new TPushPipeMetaResp(new 
TSStatus(TSStatusCode.PIPE_PUSH_META_ERROR.getStatusCode()))
+            
.setExceptionMessages(Collections.singletonList(exceptionMessage)));
+
+    final ConfigNodeProcedureEnv env = 
Mockito.mock(ConfigNodeProcedureEnv.class);
+    final ConfigManager configManager = Mockito.mock(ConfigManager.class);
+    final ConsensusManager consensusManager = 
Mockito.mock(ConsensusManager.class);
+    Mockito.when(env.getConfigManager()).thenReturn(configManager);
+    
Mockito.when(configManager.getConsensusManager()).thenReturn(consensusManager);
+    Mockito.when(consensusManager.write(Mockito.any(ConfigPhysicalPlan.class)))
+        .thenReturn(new 
TSStatus(TSStatusCode.EXECUTE_STATEMENT_ERROR.getStatusCode()))
+        .thenReturn(new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()));
+    
Mockito.when(env.pushSinglePipeMetaToDataNodes(Mockito.any(ByteBuffer.class), 
Mockito.any()))
+        .thenReturn(responses);
+
+    try {
+      procedure.executeFromOperateOnDataNodes(env);
+      fail("Expected the first attempt to fail its ConfigNode consensus 
write");
+    } catch (final org.apache.iotdb.pipe.api.exception.PipeException expected) 
{
+      // The procedure retries this state after the persistence failure.
+    }
+
+    responses.put(
+        1, new TPushPipeMetaResp(new 
TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode())));
+    procedure.executeFromOperateOnDataNodes(env);
+
+    final ArgumentCaptor<ConfigPhysicalPlan> planCaptor =
+        ArgumentCaptor.forClass(ConfigPhysicalPlan.class);
+    Mockito.verify(consensusManager, 
Mockito.times(2)).write(planCaptor.capture());
+    assertTrue(planCaptor.getAllValues().get(1) instanceof 
PipeHandleMetaChangePlan);
+    final PipeRuntimeMeta persistedRuntimeMeta =
+        ((PipeHandleMetaChangePlan) planCaptor.getAllValues().get(1))
+            .getPipeMetaList().stream()
+                .filter(meta -> 
request.getPipeName().equals(meta.getStaticMeta().getPipeName()))
+                .findFirst()
+                .orElseThrow(AssertionError::new)
+                .getRuntimeMeta();
+    assertEquals(PipeStatus.STOPPED, persistedRuntimeMeta.getStatus().get());
+    assertTrue(persistedRuntimeMeta.getIsStoppedByRuntimeException());
+    assertEquals(
+        error, 
persistedRuntimeMeta.getNodeId2PipeRuntimeExceptionMap().get(1).getMessage());
+  }
+
+  private static ConsensusManager mockSuccessfulConsensusWrite(final 
ConfigNodeProcedureEnv env)
+      throws Exception {
+    final ConfigManager configManager = Mockito.mock(ConfigManager.class);
+    final ConsensusManager consensusManager = 
Mockito.mock(ConsensusManager.class);
+    Mockito.when(env.getConfigManager()).thenReturn(configManager);
+    
Mockito.when(configManager.getConsensusManager()).thenReturn(consensusManager);
+    Mockito.when(consensusManager.write(Mockito.any(ConfigPhysicalPlan.class)))
+        .thenReturn(new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()));
+    return consensusManager;
+  }
+
+  private static void assertPipeMetaChangeWasWritten(
+      final ConsensusManager consensusManager, final String pipeName, final 
String error)
+      throws Exception {
+    final ArgumentCaptor<ConfigPhysicalPlan> planCaptor =
+        ArgumentCaptor.forClass(ConfigPhysicalPlan.class);
+    Mockito.verify(consensusManager).write(planCaptor.capture());
+    assertTrue(planCaptor.getValue() instanceof PipeHandleMetaChangePlan);
+    final PipeRuntimeMeta persistedRuntimeMeta =
+        ((PipeHandleMetaChangePlan) planCaptor.getValue())
+            .getPipeMetaList().stream()
+                .filter(meta -> 
pipeName.equals(meta.getStaticMeta().getPipeName()))
+                .findFirst()
+                .orElseThrow(AssertionError::new)
+                .getRuntimeMeta();
+    assertEquals(PipeStatus.STOPPED, persistedRuntimeMeta.getStatus().get());
+    assertTrue(persistedRuntimeMeta.getIsStoppedByRuntimeException());
+    assertEquals(
+        error, 
persistedRuntimeMeta.getNodeId2PipeRuntimeExceptionMap().get(1).getMessage());
+  }
+
+  private static TCreatePipeReq createPipeRequest(final String pipeName) {
+    final Map<String, String> extractorAttributes = new HashMap<>();
+    
extractorAttributes.put(PipeSourceConstant.EXTRACTOR_CONSENSUS_GROUP_ID_KEY, 
"DataRegion[1]");
+    
extractorAttributes.put(PipeSourceConstant.EXTRACTOR_CONSENSUS_SENDER_DATANODE_ID_KEY,
 "1");
+    return new TCreatePipeReq(PipeStaticMeta.CONSENSUS_PIPE_PREFIX + pipeName, 
new HashMap<>())
+        .setExtractorAttributes(extractorAttributes)
+        .setProcessorAttributes(new HashMap<>());
+  }
+
   @Test
   public void serializeDeserializeTest() {
     PublicBAOS byteArrayOutputStream = new PublicBAOS();
diff --git 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/pipe/task/StartPipeProcedureV2Test.java
 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/pipe/task/StartPipeProcedureV2Test.java
index a6234421742..8a1796d4fd3 100644
--- 
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/pipe/task/StartPipeProcedureV2Test.java
+++ 
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/pipe/task/StartPipeProcedureV2Test.java
@@ -19,18 +19,227 @@
 
 package org.apache.iotdb.confignode.procedure.impl.pipe.task;
 
+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.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;
+import org.apache.iotdb.confignode.consensus.request.ConfigPhysicalPlan;
+import 
org.apache.iotdb.confignode.consensus.request.write.pipe.runtime.PipeHandleMetaChangePlan;
+import 
org.apache.iotdb.confignode.consensus.request.write.pipe.task.CreatePipePlanV2;
+import 
org.apache.iotdb.confignode.consensus.request.write.pipe.task.SetPipeStatusPlanV2;
+import org.apache.iotdb.confignode.manager.ConfigManager;
+import org.apache.iotdb.confignode.manager.consensus.ConsensusManager;
+import org.apache.iotdb.confignode.persistence.pipe.PipeTaskInfo;
+import org.apache.iotdb.confignode.procedure.env.ConfigNodeProcedureEnv;
 import org.apache.iotdb.confignode.procedure.store.ProcedureFactory;
+import org.apache.iotdb.mpp.rpc.thrift.TPushPipeMetaResp;
+import org.apache.iotdb.mpp.rpc.thrift.TPushPipeMetaRespExceptionMessage;
+import org.apache.iotdb.rpc.TSStatusCode;
 
 import org.apache.tsfile.utils.PublicBAOS;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
 
 import java.io.DataOutputStream;
 import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicReference;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 public class StartPipeProcedureV2Test {
+  @Test
+  public void testDataNodeConstructionFailureIsRecorded() throws Exception {
+    final String pipeName = "startFailurePipe";
+    final PipeTaskInfo taskInfo = new PipeTaskInfo();
+    final PipeStaticMeta staticMeta =
+        new PipeStaticMeta(
+            pipeName,
+            System.currentTimeMillis(),
+            Collections.singletonMap("extractor", "iotdb-source"),
+            Collections.singletonMap("processor", "do-nothing-processor"),
+            Collections.singletonMap("connector", "write-back-sink"));
+    final PipeRuntimeMeta runtimeMeta = new PipeRuntimeMeta(new 
ConcurrentHashMap<>());
+    runtimeMeta
+        .getConsensusGroupId2TaskMetaMap()
+        .put(1, new PipeTaskMeta(MinimumProgressIndex.INSTANCE, 1));
+    taskInfo.createPipe(new CreatePipePlanV2(staticMeta, runtimeMeta));
+    taskInfo.setPipeStatus(new SetPipeStatusPlanV2(pipeName, 
PipeStatus.RUNNING));
+
+    final StartPipeProcedureV2 procedure = new 
TestableStartPipeProcedureV2(pipeName, taskInfo);
+    final ConfigNodeProcedureEnv env = 
Mockito.mock(ConfigNodeProcedureEnv.class);
+    final ConsensusManager consensusManager = 
mockSuccessfulConsensusWrite(env);
+    final String error = "Failed to construct PipeConnector";
+    final TPushPipeMetaRespExceptionMessage exceptionMessage =
+        new TPushPipeMetaRespExceptionMessage(pipeName, error, 
System.currentTimeMillis())
+            .setCreationTime(staticMeta.getCreationTime());
+    
Mockito.when(env.pushSinglePipeMetaToDataNodes(Mockito.any(ByteBuffer.class), 
Mockito.any()))
+        .thenReturn(
+            Collections.singletonMap(
+                1,
+                new TPushPipeMetaResp(
+                        new 
TSStatus(TSStatusCode.PIPE_PUSH_META_ERROR.getStatusCode()))
+                    
.setExceptionMessages(Collections.singletonList(exceptionMessage))));
+
+    procedure.executeFromOperateOnDataNodes(env);
+
+    final PipeRuntimeMeta recordedRuntimeMeta =
+        taskInfo.getPipeMetaByPipeName(pipeName).getRuntimeMeta();
+    assertEquals(PipeStatus.STOPPED, recordedRuntimeMeta.getStatus().get());
+    assertEquals(
+        error, 
recordedRuntimeMeta.getNodeId2PipeRuntimeExceptionMap().get(1).getMessage());
+    assertPipeMetaChangeWasWritten(consensusManager, pipeName, error);
+  }
+
+  @Test
+  public void 
testFailureWithoutExceptionDetailsIsNotTreatedAsSuccessfulStart() throws 
Exception {
+    final String pipeName = "startFailureWithoutDetailsPipe";
+    final PipeTaskInfo taskInfo = new PipeTaskInfo();
+    final PipeStaticMeta staticMeta =
+        new PipeStaticMeta(
+            pipeName,
+            System.currentTimeMillis(),
+            Collections.singletonMap("extractor", "iotdb-source"),
+            Collections.singletonMap("processor", "do-nothing-processor"),
+            Collections.singletonMap("connector", "write-back-sink"));
+    final PipeRuntimeMeta runtimeMeta = new PipeRuntimeMeta(new 
ConcurrentHashMap<>());
+    runtimeMeta
+        .getConsensusGroupId2TaskMetaMap()
+        .put(1, new PipeTaskMeta(MinimumProgressIndex.INSTANCE, 1));
+    taskInfo.createPipe(new CreatePipePlanV2(staticMeta, runtimeMeta));
+    taskInfo.setPipeStatus(new SetPipeStatusPlanV2(pipeName, 
PipeStatus.RUNNING));
+
+    final StartPipeProcedureV2 procedure = new 
TestableStartPipeProcedureV2(pipeName, taskInfo);
+    final ConfigNodeProcedureEnv env = 
Mockito.mock(ConfigNodeProcedureEnv.class);
+    final ConsensusManager consensusManager = 
mockSuccessfulConsensusWrite(env);
+    final String error = "DataNode failed before returning pipe-specific 
details";
+    
Mockito.when(env.pushSinglePipeMetaToDataNodes(Mockito.any(ByteBuffer.class), 
Mockito.any()))
+        .thenReturn(
+            Collections.singletonMap(
+                1,
+                new TPushPipeMetaResp(
+                        new 
TSStatus(TSStatusCode.PIPE_PUSH_META_ERROR.getStatusCode())
+                            .setMessage(error))
+                    .setExceptionMessages(Collections.emptyList())));
+
+    procedure.executeFromOperateOnDataNodes(env);
+
+    final PipeRuntimeMeta recordedRuntimeMeta =
+        taskInfo.getPipeMetaByPipeName(pipeName).getRuntimeMeta();
+    assertEquals(PipeStatus.STOPPED, recordedRuntimeMeta.getStatus().get());
+    assertTrue(recordedRuntimeMeta.getIsStoppedByRuntimeException());
+    assertTrue(
+        recordedRuntimeMeta
+            .getNodeId2PipeRuntimeExceptionMap()
+            .get(1)
+            .getMessage()
+            .contains(error));
+    assertPipeMetaChangeWasWritten(consensusManager, pipeName, error);
+  }
+
+  @Test
+  public void testRetryAfterFailureToPersistRuntimeStopDoesNotClearFailure() 
throws Exception {
+    final String pipeName = "startFailurePersistRetryPipe";
+    final PipeTaskInfo taskInfo = new PipeTaskInfo();
+    final PipeStaticMeta staticMeta =
+        new PipeStaticMeta(
+            pipeName,
+            System.currentTimeMillis(),
+            Collections.singletonMap("extractor", "iotdb-source"),
+            Collections.singletonMap("processor", "do-nothing-processor"),
+            Collections.singletonMap("connector", "write-back-sink"));
+    final PipeRuntimeMeta runtimeMeta = new PipeRuntimeMeta(new 
ConcurrentHashMap<>());
+    runtimeMeta
+        .getConsensusGroupId2TaskMetaMap()
+        .put(1, new PipeTaskMeta(MinimumProgressIndex.INSTANCE, 1));
+    taskInfo.createPipe(new CreatePipePlanV2(staticMeta, runtimeMeta));
+    taskInfo.setPipeStatus(new SetPipeStatusPlanV2(pipeName, 
PipeStatus.RUNNING));
+
+    final StartPipeProcedureV2 procedure = new 
TestableStartPipeProcedureV2(pipeName, taskInfo);
+    final ConfigNodeProcedureEnv env = 
Mockito.mock(ConfigNodeProcedureEnv.class);
+    final ConfigManager configManager = Mockito.mock(ConfigManager.class);
+    final ConsensusManager consensusManager = 
Mockito.mock(ConsensusManager.class);
+    Mockito.when(env.getConfigManager()).thenReturn(configManager);
+    
Mockito.when(configManager.getConsensusManager()).thenReturn(consensusManager);
+    Mockito.when(consensusManager.write(Mockito.any(ConfigPhysicalPlan.class)))
+        .thenReturn(new 
TSStatus(TSStatusCode.EXECUTE_STATEMENT_ERROR.getStatusCode()))
+        .thenReturn(new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()));
+
+    final String error = "Failed to construct PipeConnector";
+    final TPushPipeMetaRespExceptionMessage exceptionMessage =
+        new TPushPipeMetaRespExceptionMessage(pipeName, error, 
System.currentTimeMillis())
+            .setCreationTime(staticMeta.getCreationTime());
+    
Mockito.when(env.pushSinglePipeMetaToDataNodes(Mockito.any(ByteBuffer.class), 
Mockito.any()))
+        .thenReturn(
+            Collections.singletonMap(
+                1,
+                new TPushPipeMetaResp(
+                        new 
TSStatus(TSStatusCode.PIPE_PUSH_META_ERROR.getStatusCode()))
+                    
.setExceptionMessages(Collections.singletonList(exceptionMessage))));
+
+    try {
+      procedure.executeFromOperateOnDataNodes(env);
+      fail("Expected the first attempt to fail its ConfigNode consensus 
write");
+    } catch (final org.apache.iotdb.pipe.api.exception.PipeException expected) 
{
+      // The procedure retries this state after the persistence failure.
+    }
+
+    final PipeRuntimeMeta runtimeMetaAfterFirstAttempt =
+        taskInfo.getPipeMetaByPipeName(pipeName).getRuntimeMeta();
+    assertEquals(PipeStatus.STOPPED, 
runtimeMetaAfterFirstAttempt.getStatus().get());
+    assertTrue(runtimeMetaAfterFirstAttempt.getIsStoppedByRuntimeException());
+
+    procedure.executeFromOperateOnDataNodes(env);
+
+    final PipeRuntimeMeta runtimeMetaAfterRetry =
+        taskInfo.getPipeMetaByPipeName(pipeName).getRuntimeMeta();
+    assertEquals(PipeStatus.STOPPED, runtimeMetaAfterRetry.getStatus().get());
+    assertTrue(runtimeMetaAfterRetry.getIsStoppedByRuntimeException());
+    assertEquals(
+        error, 
runtimeMetaAfterRetry.getNodeId2PipeRuntimeExceptionMap().get(1).getMessage());
+    Mockito.verify(env, Mockito.times(1))
+        .pushSinglePipeMetaToDataNodes(Mockito.any(ByteBuffer.class), 
Mockito.any());
+    Mockito.verify(consensusManager, 
Mockito.times(2)).write(Mockito.any(ConfigPhysicalPlan.class));
+  }
+
+  private static ConsensusManager mockSuccessfulConsensusWrite(final 
ConfigNodeProcedureEnv env)
+      throws Exception {
+    final ConfigManager configManager = Mockito.mock(ConfigManager.class);
+    final ConsensusManager consensusManager = 
Mockito.mock(ConsensusManager.class);
+    Mockito.when(env.getConfigManager()).thenReturn(configManager);
+    
Mockito.when(configManager.getConsensusManager()).thenReturn(consensusManager);
+    Mockito.when(consensusManager.write(Mockito.any(ConfigPhysicalPlan.class)))
+        .thenReturn(new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()));
+    return consensusManager;
+  }
+
+  private static void assertPipeMetaChangeWasWritten(
+      final ConsensusManager consensusManager, final String pipeName, final 
String error)
+      throws Exception {
+    final ArgumentCaptor<ConfigPhysicalPlan> planCaptor =
+        ArgumentCaptor.forClass(ConfigPhysicalPlan.class);
+    Mockito.verify(consensusManager).write(planCaptor.capture());
+    assertTrue(planCaptor.getValue() instanceof PipeHandleMetaChangePlan);
+
+    final PipeRuntimeMeta persistedRuntimeMeta =
+        ((PipeHandleMetaChangePlan) planCaptor.getValue())
+            .getPipeMetaList().stream()
+                .filter(meta -> 
pipeName.equals(meta.getStaticMeta().getPipeName()))
+                .findFirst()
+                .orElseThrow(AssertionError::new)
+                .getRuntimeMeta();
+    assertEquals(PipeStatus.STOPPED, persistedRuntimeMeta.getStatus().get());
+    assertTrue(persistedRuntimeMeta.getIsStoppedByRuntimeException());
+    assertEquals(
+        error, 
persistedRuntimeMeta.getNodeId2PipeRuntimeExceptionMap().get(1).getMessage());
+  }
+
   @Test
   public void serializeDeserializeTest() {
     PublicBAOS byteArrayOutputStream = new PublicBAOS();
@@ -70,4 +279,12 @@ public class StartPipeProcedureV2Test {
       fail();
     }
   }
+
+  private static class TestableStartPipeProcedureV2 extends 
StartPipeProcedureV2 {
+    private TestableStartPipeProcedureV2(final String pipeName, final 
PipeTaskInfo taskInfo)
+        throws Exception {
+      super(pipeName);
+      pipeTaskInfo = new AtomicReference<>(taskInfo);
+    }
+  }
 }
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/sink/PipeSinkTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/sink/PipeSinkTest.java
index ca7ac8d2ba0..5f65556398d 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/sink/PipeSinkTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/sink/PipeSinkTest.java
@@ -460,6 +460,37 @@ public class PipeSinkTest {
         () -> assertWriteBackSinkTargetDatabaseValid("root." + 
"a".repeat(60)));
   }
 
+  @Test
+  public void testWriteBackSinkDatabaseValidationAfterCloningParameters() 
throws Exception {
+    for (final String databaseKey : Arrays.asList("sink.database", 
"connector.database")) {
+      final PipeParameters parameters =
+          new PipeParameters(Collections.singletonMap(databaseKey, 
"root.target"))
+              .addOrReplaceEquivalentAttributesWithClone(
+                  new PipeParameters(
+                      Collections.singletonMap(
+                          PipeSinkConstant.SINK_ENABLE_SEND_TSFILE_LIMIT, 
"true")));
+
+      try (final WriteBackSink sink = new WriteBackSink()) {
+        sink.validate(new PipeParameterValidator(parameters));
+        sink.customize(
+            parameters,
+            new PipeTaskRuntimeConfiguration(new 
PipeTaskSinkRuntimeEnvironment("pipe", 1L, 1)));
+        Assert.assertEquals(
+            "root.target", getWriteBackSinkDatabaseName(sink, 
"targetTreeModelDatabaseName"));
+      }
+    }
+
+    final HashMap<String, String> conflictingAttributes = new HashMap<>();
+    conflictingAttributes.put("connector.database", "root.connectorTarget");
+    conflictingAttributes.put("sink.database", "root.sinkTarget");
+    try (final WriteBackSink sink = new WriteBackSink()) {
+      Assert.assertThrows(
+          PipeException.class,
+          () ->
+              sink.validate(new PipeParameterValidator(new 
PipeParameters(conflictingAttributes))));
+    }
+  }
+
   @Test
   public void testWriteBackSinkTargetDatabaseCustomization() throws Exception {
     try (final WriteBackSink sink = 
createCustomizedWriteBackSink("TestTarget")) {
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/config/sys/pipe/ShowPipeTaskTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/config/sys/pipe/ShowPipeTaskTest.java
index 0822b51f1cb..a591627e588 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/config/sys/pipe/ShowPipeTaskTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/config/sys/pipe/ShowPipeTaskTest.java
@@ -19,6 +19,7 @@
 
 package org.apache.iotdb.db.queryengine.plan.execution.config.sys.pipe;
 
+import org.apache.iotdb.commons.pipe.agent.task.meta.PipeStatus;
 import org.apache.iotdb.commons.schema.column.ColumnHeaderConstant;
 import org.apache.iotdb.confignode.rpc.thrift.TShowPipeInfo;
 import org.apache.iotdb.db.queryengine.plan.execution.config.ConfigTaskResult;
@@ -41,6 +42,8 @@ public class ShowPipeTaskTest {
   @Test
   public void testBuildTSBlockWritesRuntimeColumns() throws Exception {
     final TShowPipeInfo degradedPipe = createPipeInfo("degraded_pipe");
+    degradedPipe.setState(PipeStatus.STOPPED.name());
+    degradedPipe.setExceptionMessage("nodeIds: [1], construction failed");
     degradedPipe.setIsDegraded(true);
     final Map<String, Long> recentFailures = new HashMap<>();
     recentFailures.put("network_timeout", 10L);
@@ -62,6 +65,10 @@ public class ShowPipeTaskTest {
     assertEquals(
         ColumnHeaderConstant.RECENT_FAILURES, 
result.getResultSetHeader().getRespColumns().get(10));
     assertEquals(3, resultSet.getPositionCount());
+    assertEquals(PipeStatus.STOPPED.name(), 
resultSet.getColumn(2).getBinary(0).toString());
+    assertEquals(
+        "nodeIds: [1], construction failed", 
resultSet.getColumn(6).getBinary(0).toString());
+    assertEquals("", resultSet.getColumn(6).getBinary(1).toString());
     assertTrue(resultSet.getColumn(9).getBoolean(0));
     assertFalse(resultSet.getColumn(9).getBoolean(1));
     assertTrue(resultSet.getColumn(9).isNull(2));

Reply via email to