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

jt2594838 pushed a commit to branch dev/1.3
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/dev/1.3 by this push:
     new 7f6e63f726f [To dev/1.3] Fix write-back sink history failure state 
handling (#19072)
7f6e63f726f is described below

commit 7f6e63f726fff9cfd4a112d80a5f6930192abb27
Author: Caideyipi <[email protected]>
AuthorDate: Mon Nov 2 16:20:49 2026 +0800

    [To dev/1.3] Fix write-back sink history failure state handling (#19072)
    
    * 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
    
    (cherry picked from commit 595aa8442bb50fc8bfafdb8f11d40a94e1b3c1bd)
    
    * Adapt key reducer visibility for the 1.3 backport
    
    * Add test dependency for the Pipe API backport
---
 .../pipe/it/autocreate/IoTDBPipeAutoSplitIT.java   |  55 +++++
 iotdb-api/pipe-api/pom.xml                         |   5 +
 .../parameter/PipeParameterValidator.java          |  24 ++-
 .../api/customizer/parameter/PipeParameters.java   |   2 +-
 .../parameter/PipeParameterValidatorTest.java      | 126 +++++++++++
 iotdb-core/confignode/pom.xml                      |  38 ++++
 .../iotdb/confignode/i18n/ProcedureMessages.java   |  32 +++
 .../iotdb/confignode/i18n/ProcedureMessages.java   |  32 +++
 .../confignode/persistence/pipe/PipeTaskInfo.java  | 134 ++++++++----
 .../impl/pipe/AbstractOperatePipeProcedureV2.java  |  83 +++++--
 .../impl/pipe/runtime/PipeMetaSyncProcedure.java   |   4 +
 .../impl/pipe/task/AlterPipeProcedureV2.java       |  20 +-
 .../impl/pipe/task/CreatePipeProcedureV2.java      |  10 +-
 .../impl/pipe/task/StartPipeProcedureV2.java       |  50 ++---
 .../consensus/response/pipe/PipeTableRespTest.java |  20 ++
 .../pipe/PipeTaskInfoAutoRestartTest.java          |  56 +++++
 .../pipe/AbstractOperatePipeProcedureV2Test.java   |  24 +++
 .../pipe/runtime/PipeMetaSyncProcedureTest.java    | 239 +++++++++++++++++++++
 .../impl/pipe/task/CreatePipeProcedureV2Test.java  | 144 +++++++++++++
 .../impl/pipe/task/StartPipeProcedureV2Test.java   | 215 ++++++++++++++++++
 .../apache/iotdb/db/pipe/sink/PipeSinkTest.java    |  36 ++++
 .../config/sys/pipe/ShowPipeTaskTest.java          |   7 +
 22 files changed, 1260 insertions(+), 96 deletions(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/pipe/it/autocreate/IoTDBPipeAutoSplitIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/pipe/it/autocreate/IoTDBPipeAutoSplitIT.java
index ced1e9ee13c..afa30ada696 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/pipe/it/autocreate/IoTDBPipeAutoSplitIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/pipe/it/autocreate/IoTDBPipeAutoSplitIT.java
@@ -20,6 +20,7 @@
 package org.apache.iotdb.pipe.it.autocreate;
 
 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;
@@ -40,6 +41,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;
 
@@ -77,6 +79,59 @@ public class IoTDBPipeAutoSplitIT extends 
AbstractPipeDualAutoIT {
     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()).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/pom.xml b/iotdb-api/pipe-api/pom.xml
index bae1e57a958..97580ffa452 100644
--- a/iotdb-api/pipe-api/pom.xml
+++ b/iotdb-api/pipe-api/pom.xml
@@ -35,6 +35,11 @@
             <version>${tsfile.version}</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
     <profiles>
         <profile>
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 99d547a23c6..d4e6c866d45 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
@@ -25,6 +25,7 @@ import 
org.apache.iotdb.pipe.api.exception.PipeParameterNotValidException;
 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;
 
@@ -57,9 +58,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(
               "Cannot specify both %s and %s at the same time", lhsAttributes, 
rhsAttributes));
@@ -74,6 +80,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/main/java/org/apache/iotdb/pipe/api/customizer/parameter/PipeParameters.java
 
b/iotdb-api/pipe-api/src/main/java/org/apache/iotdb/pipe/api/customizer/parameter/PipeParameters.java
index b6d59c827f0..bda09613c3d 100644
--- 
a/iotdb-api/pipe-api/src/main/java/org/apache/iotdb/pipe/api/customizer/parameter/PipeParameters.java
+++ 
b/iotdb-api/pipe-api/src/main/java/org/apache/iotdb/pipe/api/customizer/parameter/PipeParameters.java
@@ -355,7 +355,7 @@ public class PipeParameters {
     return new PipeParameters(thisMap);
   }
 
-  private static class KeyReducer {
+  static class KeyReducer {
     private static final Set<String> FIRST_PREFIXES = new HashSet<>();
     private static final Set<String> SECOND_PREFIXES = new HashSet<>();
 
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/pom.xml b/iotdb-core/confignode/pom.xml
index a7cd4b6298b..f7fd6c5046b 100644
--- a/iotdb-core/confignode/pom.xml
+++ b/iotdb-core/confignode/pom.xml
@@ -32,6 +32,7 @@
         <confignode.it.skip>${confignode.test.skip}</confignode.it.skip>
         <confignode.test.skip>false</confignode.test.skip>
         <confignode.ut.skip>${confignode.test.skip}</confignode.ut.skip>
+        <i18n.locale>en</i18n.locale>
     </properties>
     <dependencies>
         <dependency>
@@ -168,6 +169,37 @@
     </dependencies>
     <build>
         <plugins>
+            <plugin>
+                <groupId>com.diffplug.spotless</groupId>
+                <artifactId>spotless-maven-plugin</artifactId>
+                <configuration>
+                    <java>
+                        <includes>
+                            <include>src/main/java/**/*.java</include>
+                            <include>src/test/java/**/*.java</include>
+                            <include>src/main/i18n/**/*.java</include>
+                        </includes>
+                    </java>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>build-helper-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>add-i18n-source</id>
+                        <phase>generate-sources</phase>
+                        <goals>
+                            <goal>add-source</goal>
+                        </goals>
+                        <configuration>
+                            <sources>
+                                
<source>${project.basedir}/src/main/i18n/${i18n.locale}</source>
+                            </sources>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-assembly-plugin</artifactId>
@@ -241,6 +273,12 @@
         </plugins>
     </build>
     <profiles>
+        <profile>
+            <id>with-zh-locale</id>
+            <properties>
+                <i18n.locale>zh</i18n.locale>
+            </properties>
+        </profile>
         <profile>
             <id>skipConfigNodeTests</id>
             <activation>
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
new file mode 100644
index 00000000000..c80cb1f7cc4
--- /dev/null
+++ 
b/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ProcedureMessages.java
@@ -0,0 +1,32 @@
+/*
+ * 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.i18n;
+
+public final class ProcedureMessages {
+
+  private ProcedureMessages() {}
+
+  public static final String
+      
EXCEPTION_INTERNAL_ERROR_WHILE_PROCESSING_PUSHPIPEMETA_ON_DATANODE_5983BD5F =
+          "Internal error while processing pushPipeMeta on DataNode.";
+  public static final String
+      
LOG_FAILED_IN_THE_WRITE_API_EXECUTING_THE_CONSENSUS_LAYER_DUE_TO_8DDF6FE9 =
+          "Failed in the write API executing the consensus layer due to: ";
+}
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
new file mode 100644
index 00000000000..7ede13f18dc
--- /dev/null
+++ 
b/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ProcedureMessages.java
@@ -0,0 +1,32 @@
+/*
+ * 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.i18n;
+
+public final class ProcedureMessages {
+
+  private ProcedureMessages() {}
+
+  public static final String
+      
EXCEPTION_INTERNAL_ERROR_WHILE_PROCESSING_PUSHPIPEMETA_ON_DATANODE_5983BD5F =
+          "处理 DataNode 上的 pushPipeMeta 时发生内部错误。";
+  public static final String
+      
LOG_FAILED_IN_THE_WRITE_API_EXECUTING_THE_CONSENSUS_LAYER_DUE_TO_8DDF6FE9 =
+          "执行共识层写入 API 时失败,原因:";
+}
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 3e7dcbdfa01..02e7aafb79c 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
@@ -50,6 +50,7 @@ import 
org.apache.iotdb.confignode.consensus.request.write.pipe.task.OperateMult
 import 
org.apache.iotdb.confignode.consensus.request.write.pipe.task.SetPipeStatusPlanV2;
 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.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;
@@ -57,6 +58,7 @@ import org.apache.iotdb.confignode.rpc.thrift.TCreatePipeReq;
 import org.apache.iotdb.confignode.service.ConfigNode;
 import org.apache.iotdb.consensus.common.DataSet;
 import org.apache.iotdb.mpp.rpc.thrift.TPushPipeMetaResp;
+import org.apache.iotdb.mpp.rpc.thrift.TPushPipeMetaRespExceptionMessage;
 import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameters;
 import org.apache.iotdb.pipe.api.exception.PipeException;
 import org.apache.iotdb.rpc.TSStatusCode;
@@ -91,6 +93,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;
 
@@ -829,22 +834,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()) {
@@ -854,50 +880,84 @@ 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;
         }
 
-        resp.getExceptionMessages().stream()
-            .filter(message -> 
pipeMetaKeeper.containsPipeMeta(message.getPipeName()))
-            .forEach(
-                message -> {
-                  final PipeRuntimeMeta runtimeMeta =
-                      
pipeMetaKeeper.getPipeMeta(message.getPipeName()).getRuntimeMeta();
-
-                  if 
(PipeStatus.PRE_DELETE.equals(runtimeMeta.getStatus().get())) {
-                    return;
-                  }
-
-                  // 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()) {
-                    return;
-                  }
-
-                  // 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()));
-                  }
-                });
+        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 = 
pipeMetaKeeper.getPipeMeta(messagePipeName);
+          if (pipeMeta == null) {
+            continue;
+          }
+
+          recordDataNodePushPipeMetaException(
+              pipeMeta, dataNodeId, message.getMessage(), 
message.getTimeStamp());
+        }
       }
     }
 
     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.getPipeName());
+      if (pipeMeta != null
+          && pipeMeta.getStaticMeta().getCreationTime()
+              == fallbackPipeStaticMeta.getCreationTime()) {
+        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 5587b51af8d..22b75a9067a 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
@@ -19,8 +19,11 @@
 
 package org.apache.iotdb.confignode.procedure.impl.pipe;
 
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
 import org.apache.iotdb.commons.pipe.agent.task.meta.PipeMeta;
 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.ProcedureMessages;
 import 
org.apache.iotdb.confignode.manager.pipe.metric.overview.PipeProcedureMetrics;
 import org.apache.iotdb.confignode.persistence.pipe.PipeTaskInfo;
 import org.apache.iotdb.confignode.procedure.Procedure;
@@ -30,6 +33,7 @@ import 
org.apache.iotdb.confignode.procedure.impl.node.AbstractNodeProcedure;
 import 
org.apache.iotdb.confignode.procedure.impl.pipe.runtime.PipeMetaSyncProcedure;
 import org.apache.iotdb.confignode.procedure.state.ProcedureLockState;
 import 
org.apache.iotdb.confignode.procedure.state.pipe.task.OperatePipeTaskState;
+import org.apache.iotdb.consensus.exception.ConsensusException;
 import org.apache.iotdb.mpp.rpc.thrift.TPushPipeMetaResp;
 import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameters;
 import org.apache.iotdb.pipe.api.exception.PipeException;
@@ -631,14 +635,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(".");
@@ -654,7 +660,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(
@@ -669,16 +675,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()));
+                    }
                   }
                 });
 
@@ -698,6 +705,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
@@ -717,6 +732,38 @@ 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(
+          ProcedureMessages
+              
.LOG_FAILED_IN_THE_WRITE_API_EXECUTING_THE_CONSENSUS_LAYER_DUE_TO_8DDF6FE9,
+          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 9764a3af3a5..d5e49878d20 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
@@ -129,6 +129,7 @@ public class PipeMetaSyncProcedure extends 
AbstractOperatePipeProcedureV2 {
     if (response.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
       throw new PipeException(response.getMessage());
     }
+    pipeTaskInfo.get().markPipeMetaExceptionsPersisted();
   }
 
   @Override
@@ -136,8 +137,11 @@ public class PipeMetaSyncProcedure extends 
AbstractOperatePipeProcedureV2 {
       throws PipeException, IOException {
     LOGGER.debug("PipeMetaSyncProcedure: executeFromOperateOnDataNodes");
 
+    persistPendingPipeMetaExceptions(env);
+
     Map<Integer, TPushPipeMetaResp> respMap = 
pushPipeMetaToDataNodesBestEffortAndGetResponse(env);
     if (pipeTaskInfo.get().recordDataNodePushPipeMetaExceptions(respMap)) {
+      writePipeMetaChangesToConfigNodeConsensus(env);
       throw new PipeException(
           String.format(
               "Failed to push pipe meta to dataNodes, details: %s",
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 f7ed7496f77..c6369364d0d 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
@@ -42,6 +42,7 @@ import 
org.apache.iotdb.confignode.procedure.impl.pipe.PipeTaskOperation;
 import org.apache.iotdb.confignode.procedure.store.ProcedureType;
 import org.apache.iotdb.confignode.rpc.thrift.TAlterPipeReq;
 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;
 
@@ -317,14 +318,17 @@ public class AlterPipeProcedureV2 extends 
AbstractOperatePipeProcedureV2 {
     final String pipeName = alterPipeRequest.getPipeName();
     LOGGER.info("AlterPipeProcedureV2: executeFromOperateOnDataNodes({})", 
pipeName);
 
-    final String exceptionMessage =
-        parsePushPipeMetaExceptionForPipe(
-            pipeName,
-            
!PipeTaskAgent.isRealtimeOnlyPipe(currentPipeStaticMeta.getExtractorParameters())
-                    && PipeTaskAgent.isRealtimeOnlyPipe(
-                        updatedPipeStaticMeta.getExtractorParameters())
-                ? pushSinglePipeMetaToDataNodes4Realtime(pipeName, env)
-                : pushSinglePipeMetaToDataNodes(pipeName, env));
+    persistPendingPipeMetaExceptions(env);
+
+    final Map<Integer, TPushPipeMetaResp> respMap =
+        
!PipeTaskAgent.isRealtimeOnlyPipe(currentPipeStaticMeta.getExtractorParameters())
+                && 
PipeTaskAgent.isRealtimeOnlyPipe(updatedPipeStaticMeta.getExtractorParameters())
+            ? pushSinglePipeMetaToDataNodes4Realtime(pipeName, env)
+            : pushSinglePipeMetaToDataNodes(pipeName, env);
+    if (pipeTaskInfo.get().recordDataNodePushPipeMetaExceptions(respMap, 
updatedPipeStaticMeta)) {
+      writePipeMetaChangesToConfigNodeConsensus(env);
+    }
+    final String exceptionMessage = 
parsePushPipeMetaExceptionForPipe(pipeName, respMap);
     if (!exceptionMessage.isEmpty()) {
       LOGGER.warn(
           "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 ad18cc95bb0..d809c1668ed 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
@@ -45,6 +45,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;
@@ -301,8 +302,13 @@ public class CreatePipeProcedureV2 extends 
AbstractOperatePipeProcedureV2 {
     final String pipeName = createPipeRequest.getPipeName();
     LOGGER.info("CreatePipeProcedureV2: executeFromOperateOnDataNodes({})", 
pipeName);
 
-    final String exceptionMessage =
-        parsePushPipeMetaExceptionForPipe(pipeName, 
pushSinglePipeMetaToDataNodes(pipeName, env));
+    persistPendingPipeMetaExceptions(env);
+
+    final Map<Integer, TPushPipeMetaResp> respMap = 
pushSinglePipeMetaToDataNodes(pipeName, env);
+    if (pipeTaskInfo.get().recordDataNodePushPipeMetaExceptions(respMap, 
pipeStaticMeta)) {
+      writePipeMetaChangesToConfigNodeConsensus(env);
+    }
+    final String exceptionMessage = 
parsePushPipeMetaExceptionForPipe(pipeName, respMap);
     if (!exceptionMessage.isEmpty()) {
       LOGGER.warn(
           "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 962aa341fc7..a6f17e4aecf 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
@@ -21,14 +21,15 @@ 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.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.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.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;
 
@@ -39,8 +40,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 {
@@ -106,8 +106,26 @@ public class StartPipeProcedureV2 extends 
AbstractOperatePipeProcedureV2 {
     final long exceptionsClearTime = System.currentTimeMillis();
     final boolean isStoppedByRuntimeException =
         pipeTaskInfo.get().isStoppedByRuntimeException(pipeName);
-    final String exceptionMessage =
-        parsePushPipeMetaExceptionForPipe(pipeName, 
pushSinglePipeMetaToDataNodes(pipeName, env));
+    final boolean hadPendingPipeMetaExceptions =
+        pipeTaskInfo.get().hasPendingPipeMetaExceptionPersistence();
+    if (hadPendingPipeMetaExceptions) {
+      persistPendingPipeMetaExceptions(env);
+    }
+    final PipeMeta pipeMeta = 
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(pipeName, env);
+    if (pipeTaskInfo.get().recordDataNodePushPipeMetaExceptions(respMap, 
pipeStaticMeta)) {
+      writePipeMetaChangesToConfigNodeConsensus(env);
+    }
+    final String exceptionMessage = 
parsePushPipeMetaExceptionForPipe(pipeName, respMap);
     if (!exceptionMessage.isEmpty()) {
       LOGGER.warn(
           "Failed to start pipe {}, details: {}, metadata will be synchronized 
later.",
@@ -127,28 +145,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("Failed in the write API executing the consensus layer due 
to: ", 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("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 068f85a0a3d..72fa84df690 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;
@@ -176,4 +177,23 @@ public class PipeTableRespTest {
         PipeStatus.PRE_DELETE.name(),
         
pipeTableResp.convertToTShowPipeResp().getPipeInfoList().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]"));
+  }
 }
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 6d933dbc413..80b7fd98878 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
@@ -27,6 +27,7 @@ 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.write.pipe.task.CreatePipePlanV2;
 import 
org.apache.iotdb.confignode.consensus.request.write.pipe.task.SetPipeStatusPlanV2;
+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;
@@ -71,6 +72,60 @@ public class PipeTaskInfoAutoRestartTest {
     Assert.assertEquals(PipeStatus.RUNNING, runtimeMeta.getStatus().get());
   }
 
+  @Test
+  public void 
testRecordDataNodePushPipeMetaExceptionsUsesFallbackForEmptyDetails() {
+    final String pipeName = "emptyDetailsPipe";
+    createPipe(pipeName, PipeStatus.RUNNING);
+    final PipeStaticMeta staticMeta = 
pipeTaskInfo.getPipeMetaByPipeName(pipeName).getStaticMeta();
+    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";
+    createPipe(pipeName, PipeStatus.RUNNING);
+    final PipeStaticMeta staticMeta = 
pipeTaskInfo.getPipeMetaByPipeName(pipeName).getStaticMeta();
+    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";
@@ -85,6 +140,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 39da89ffe76..f9011fc1cb0 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,23 +19,47 @@
 
 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.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
+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..7175308c555 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,216 @@ 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);
+    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 ac6ca8c740c..d34b050b61e 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,172 @@
 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);
+    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);
+    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 4584d75753a..5f9509363e4 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,225 @@
 
 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());
+    
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());
+    
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();
@@ -50,4 +257,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 62dca5a762e..a3db60fce14 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
@@ -31,6 +31,7 @@ import 
org.apache.iotdb.db.pipe.sink.protocol.thrift.async.IoTDBDataRegionAsyncS
 import 
org.apache.iotdb.db.pipe.sink.protocol.thrift.sync.IoTDBDataRegionSyncSink;
 import 
org.apache.iotdb.db.pipe.sink.protocol.websocket.WebSocketConnectorServer;
 import org.apache.iotdb.db.pipe.sink.protocol.websocket.WebSocketSink;
+import org.apache.iotdb.db.pipe.sink.protocol.writeback.WriteBackSink;
 import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameterValidator;
 import org.apache.iotdb.pipe.api.customizer.parameter.PipeParameters;
 import org.apache.iotdb.pipe.api.exception.PipeException;
@@ -42,8 +43,10 @@ import org.junit.Assert;
 import org.junit.Test;
 import org.mockito.Mockito;
 
+import java.lang.reflect.Field;
 import java.security.SecureRandom;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 
@@ -279,4 +282,37 @@ public class PipeSinkTest {
     return new PipeRawTabletInsertionEvent(
         tablet, false, pipeName, creationTime, null, null, false);
   }
+
+  @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)));
+        final Field databaseNameField =
+            
WriteBackSink.class.getDeclaredField("targetTreeModelDatabaseName");
+        databaseNameField.setAccessible(true);
+        Assert.assertEquals("root.target", databaseNameField.get(sink));
+      }
+    }
+
+    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))));
+    }
+  }
 }
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 6bd8676199c..47c0f88b264 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.confignode.rpc.thrift.TShowPipeInfo;
 import org.apache.iotdb.db.queryengine.common.header.ColumnHeaderConstant;
 import org.apache.iotdb.db.queryengine.plan.execution.config.ConfigTaskResult;
@@ -39,6 +40,8 @@ public class ShowPipeTaskTest {
   @Test
   public void testBuildTSBlockWritesRecentFailures() throws Exception {
     final TShowPipeInfo failedPipe = createPipeInfo("failed_pipe");
+    failedPipe.setState(PipeStatus.STOPPED.name());
+    failedPipe.setExceptionMessage("nodeIds: [1], construction failed");
     final Map<String, Long> recentFailures = new HashMap<>();
     recentFailures.put("network_timeout", 10L);
     recentFailures.put("memory_timeout", 15L);
@@ -55,6 +58,10 @@ public class ShowPipeTaskTest {
     assertEquals(
         ColumnHeaderConstant.RECENT_FAILURES, 
result.getResultSetHeader().getRespColumns().get(9));
     assertEquals(2, 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());
     assertEquals(
         "{memory_timeout=15, network_timeout=10}", 
resultSet.getColumn(9).getBinary(0).toString());
     assertEquals("{}", resultSet.getColumn(9).getBinary(1).toString());

Reply via email to