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

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


The following commit(s) were added to refs/heads/master by this push:
     new c0c1d562aa0 [Pipe] Reject operations while pipe is being dropped 
(#18485)
c0c1d562aa0 is described below

commit c0c1d562aa0539acc92181196049dbf48e334278
Author: Caideyipi <[email protected]>
AuthorDate: Mon Aug 24 12:04:14 2026 +0800

    [Pipe] Reject operations while pipe is being dropped (#18485)
---
 .../it/single/IoTDBPipeOperationDiagnosticsIT.java | 395 +++++++++++++++++++++
 .../pipe/coordinator/task/PipeTaskCoordinator.java |  24 ++
 .../confignode/persistence/pipe/PipeTaskInfo.java  |  11 +
 3 files changed, 430 insertions(+)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/pipe/it/single/IoTDBPipeOperationDiagnosticsIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/pipe/it/single/IoTDBPipeOperationDiagnosticsIT.java
new file mode 100644
index 00000000000..13561746aa6
--- /dev/null
+++ 
b/integration-test/src/test/java/org/apache/iotdb/pipe/it/single/IoTDBPipeOperationDiagnosticsIT.java
@@ -0,0 +1,395 @@
+/*
+ * 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.it.single;
+
+import org.apache.iotdb.common.rpc.thrift.TDataNodeLocation;
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.client.sync.SyncConfigNodeIServiceClient;
+import org.apache.iotdb.commons.cluster.NodeStatus;
+import org.apache.iotdb.confignode.i18n.ConfigNodeMessages;
+import org.apache.iotdb.confignode.rpc.thrift.TAlterPipeReq;
+import org.apache.iotdb.confignode.rpc.thrift.TCreatePipeReq;
+import org.apache.iotdb.confignode.rpc.thrift.TShowPipeReq;
+import org.apache.iotdb.confignode.rpc.thrift.TShowPipeResp;
+import org.apache.iotdb.isession.SessionConfig;
+import org.apache.iotdb.it.env.EnvFactory;
+import org.apache.iotdb.it.env.cluster.node.ConfigNodeWrapper;
+import org.apache.iotdb.it.env.cluster.node.DataNodeWrapper;
+import org.apache.iotdb.it.framework.IoTDBTestRunner;
+import org.apache.iotdb.itbase.category.ClusterIT;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import org.awaitility.Awaitility;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+@RunWith(IoTDBTestRunner.class)
+@Category({ClusterIT.class})
+public class IoTDBPipeOperationDiagnosticsIT {
+
+  private static final String PIPE_NAME = "pipe_operation_diagnostics";
+  private static final String RECOVER_PIPE_NAME = "pipe_drop_recover";
+
+  @Before
+  public void setUp() {
+    EnvFactory.getEnv()
+        .getConfig()
+        .getCommonConfig()
+        .setAutoCreateSchemaEnabled(true)
+        .setDnConnectionTimeoutMs(2000)
+        .setPipeMetaSyncerSyncIntervalMinutes(1)
+        .setPipeMemoryManagementEnabled(false)
+        .setIsPipeEnableMemoryCheck(false)
+        .setPipeAutoSplitFullEnabled(false);
+    EnvFactory.getEnv().initClusterEnvironment(1, 2);
+  }
+
+  @After
+  public void tearDown() {
+    EnvFactory.getEnv().cleanClusterEnvironment();
+  }
+
+  @Test
+  public void testDropPipeExposesPreDeleteAndRejectsConcurrentOperations() 
throws Exception {
+    final DataNodeWrapper unavailableDataNode = 
EnvFactory.getEnv().getDataNodeWrapper(1);
+    final ConfigNodeWrapper configNode = 
EnvFactory.getEnv().getConfigNodeWrapper(0);
+    createPipe(PIPE_NAME);
+
+    final int unavailableDataNodeId;
+    try (final SyncConfigNodeIServiceClient client = getLeaderClient()) {
+      unavailableDataNodeId = findDataNodeId(client, unavailableDataNode);
+    }
+
+    unavailableDataNode.stopForcibly();
+    try (BlackholeServer ignored =
+        new BlackholeServer(
+            unavailableDataNode.getInternalAddress(), 
unavailableDataNode.getInternalPort())) {
+      configNode.stopForcibly();
+      configNode.start();
+      awaitLeader();
+      try (final SyncConfigNodeIServiceClient client = getLeaderClient()) {
+        final TSStatus dropStatus = client.dropPipe(PIPE_NAME);
+        assertDropTimeout(dropStatus, unavailableDataNodeId);
+        Assert.assertTrue(ignored.getAcceptedConnectionCount() > 0);
+        assertPipeState(client, PIPE_NAME, "PRE_DELETE");
+
+        assertPipeBeingDropped(
+            client.alterPipe(newAlterPipeRequest(PIPE_NAME)),
+            String.format(
+                ConfigNodeMessages
+                    
.EXCEPTION_FAILED_TO_ALTER_PIPE_ARG_THE_PIPE_IS_BEING_DROPPED_919F1E2B,
+                PIPE_NAME));
+        assertPipeBeingDropped(
+            client.startPipe(PIPE_NAME),
+            String.format(
+                ConfigNodeMessages
+                    
.EXCEPTION_FAILED_TO_START_PIPE_ARG_THE_PIPE_IS_BEING_DROPPED_B41F4638,
+                PIPE_NAME));
+        assertPipeBeingDropped(
+            client.stopPipe(PIPE_NAME),
+            String.format(
+                ConfigNodeMessages
+                    
.EXCEPTION_FAILED_TO_STOP_PIPE_ARG_THE_PIPE_IS_BEING_DROPPED_37AFB22B,
+                PIPE_NAME));
+      }
+    } finally {
+      if (!configNode.isAlive()) {
+        configNode.start();
+      }
+      restartDataNode(unavailableDataNode);
+    }
+
+    awaitPipeAbsent(PIPE_NAME);
+  }
+
+  @Test
+  public void testDropPipeProcedureRecoversWithPreDelete() throws Exception {
+    final DataNodeWrapper unavailableDataNode = 
EnvFactory.getEnv().getDataNodeWrapper(1);
+    final ConfigNodeWrapper configNode = 
EnvFactory.getEnv().getConfigNodeWrapper(0);
+    createPipe(RECOVER_PIPE_NAME);
+
+    final int unavailableDataNodeId;
+    try (final SyncConfigNodeIServiceClient client = getLeaderClient()) {
+      unavailableDataNodeId = findDataNodeId(client, unavailableDataNode);
+    }
+
+    unavailableDataNode.stopForcibly();
+    try {
+      try (BlackholeServer ignored =
+          new BlackholeServer(
+              unavailableDataNode.getInternalAddress(), 
unavailableDataNode.getInternalPort())) {
+        configNode.stopForcibly();
+        configNode.start();
+        awaitLeader();
+        try (final SyncConfigNodeIServiceClient client = getLeaderClient()) {
+          final TSStatus dropStatus = client.dropPipe(RECOVER_PIPE_NAME);
+          assertDropTimeout(dropStatus, unavailableDataNodeId);
+          assertPipeState(client, RECOVER_PIPE_NAME, "PRE_DELETE");
+        }
+
+        configNode.stopForcibly();
+        configNode.start();
+        awaitLeader();
+        try (final SyncConfigNodeIServiceClient recoveredClient = 
getLeaderClient()) {
+          assertPipeState(recoveredClient, RECOVER_PIPE_NAME, "PRE_DELETE");
+          assertPipeBeingDropped(
+              
recoveredClient.alterPipe(newAlterPipeRequest(RECOVER_PIPE_NAME)),
+              String.format(
+                  ConfigNodeMessages
+                      
.EXCEPTION_FAILED_TO_ALTER_PIPE_ARG_THE_PIPE_IS_BEING_DROPPED_919F1E2B,
+                  RECOVER_PIPE_NAME));
+          assertPipeBeingDropped(
+              recoveredClient.startPipe(RECOVER_PIPE_NAME),
+              String.format(
+                  ConfigNodeMessages
+                      
.EXCEPTION_FAILED_TO_START_PIPE_ARG_THE_PIPE_IS_BEING_DROPPED_B41F4638,
+                  RECOVER_PIPE_NAME));
+          assertPipeBeingDropped(
+              recoveredClient.stopPipe(RECOVER_PIPE_NAME),
+              String.format(
+                  ConfigNodeMessages
+                      
.EXCEPTION_FAILED_TO_STOP_PIPE_ARG_THE_PIPE_IS_BEING_DROPPED_37AFB22B,
+                  RECOVER_PIPE_NAME));
+        }
+      }
+    } finally {
+      if (!configNode.isAlive()) {
+        configNode.start();
+      }
+      restartDataNode(unavailableDataNode);
+    }
+
+    awaitPipeAbsent(RECOVER_PIPE_NAME);
+  }
+
+  private void createPipe(final String pipeName) throws Exception {
+    final HashMap<String, String> sinkAttributes = new HashMap<>();
+    sinkAttributes.put("sink", "write-back-sink");
+    try (final SyncConfigNodeIServiceClient client = getLeaderClient()) {
+      final TSStatus status =
+          client.createPipe(
+              new TCreatePipeReq(pipeName, sinkAttributes)
+                  .setExtractorAttributes(new HashMap<>())
+                  .setProcessorAttributes(new HashMap<>()));
+      Assert.assertEquals(TSStatusCode.SUCCESS_STATUS.getStatusCode(), 
status.getCode());
+    }
+  }
+
+  private SyncConfigNodeIServiceClient getLeaderClient() throws Exception {
+    return (SyncConfigNodeIServiceClient) 
EnvFactory.getEnv().getLeaderConfigNodeConnection();
+  }
+
+  private static TAlterPipeReq newAlterPipeRequest(final String pipeName) {
+    return new TAlterPipeReq()
+        .setPipeName(pipeName)
+        .setExtractorAttributes(new HashMap<>())
+        .setIsReplaceAllExtractorAttributes(false)
+        .setProcessorAttributes(new HashMap<>())
+        .setIsReplaceAllProcessorAttributes(false)
+        .setConnectorAttributes(new HashMap<>())
+        .setIsReplaceAllConnectorAttributes(false);
+  }
+
+  private static int findDataNodeId(
+      final SyncConfigNodeIServiceClient client, final DataNodeWrapper 
dataNodeWrapper)
+      throws Exception {
+    for (final TDataNodeLocation dataNodeLocation : 
client.showCluster().getDataNodeList()) {
+      if (dataNodeLocation.getInternalEndPoint().getPort() == 
dataNodeWrapper.getInternalPort()) {
+        return dataNodeLocation.getDataNodeId();
+      }
+    }
+    Assert.fail("The DataNode is not registered: " + 
dataNodeWrapper.getInternalPort());
+    return -1;
+  }
+
+  private static void assertDropTimeout(final TSStatus status, final int 
unavailableDataNodeId) {
+    Assert.assertEquals(TSStatusCode.PIPE_ERROR.getStatusCode(), 
status.getCode());
+    Assert.assertNotNull(status.getMessage());
+    Assert.assertTrue(status.getMessage(), 
status.getMessage().contains("DROP_PIPE"));
+    Assert.assertTrue(status.getMessage(), 
status.getMessage().contains("procedureId="));
+    Assert.assertTrue(status.getMessage(), 
status.getMessage().contains("OPERATE_ON_DATA_NODES"));
+    Assert.assertTrue(
+        status.getMessage(), 
status.getMessage().contains(String.valueOf(unavailableDataNodeId)));
+  }
+
+  private static void assertPipeBeingDropped(final TSStatus status, final 
String expectedMessage) {
+    Assert.assertEquals(TSStatusCode.PIPE_ERROR.getStatusCode(), 
status.getCode());
+    Assert.assertEquals(expectedMessage, status.getMessage());
+  }
+
+  private static void assertPipeState(
+      final SyncConfigNodeIServiceClient client, final String pipeName, final 
String expectedState)
+      throws Exception {
+    final TShowPipeResp response =
+        client.showPipe(new 
TShowPipeReq().setUserName(SessionConfig.DEFAULT_USER));
+    Assert.assertEquals(
+        TSStatusCode.SUCCESS_STATUS.getStatusCode(), 
response.getStatus().getCode());
+    Assert.assertTrue(
+        response.getPipeInfoList().stream()
+            .anyMatch(
+                pipeInfo ->
+                    pipeName.equals(pipeInfo.getId())
+                        && expectedState.equals(pipeInfo.getState())));
+  }
+
+  private static void awaitPipeAbsent(final String pipeName) {
+    Awaitility.await()
+        .pollInterval(1, TimeUnit.SECONDS)
+        .atMost(1, TimeUnit.MINUTES)
+        .until(
+            () -> {
+              try (final SyncConfigNodeIServiceClient client = 
getLeaderClientStatic()) {
+                final TShowPipeResp response =
+                    client.showPipe(new 
TShowPipeReq().setUserName(SessionConfig.DEFAULT_USER));
+                if (response.getStatus().getCode() != 
TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+                  return false;
+                }
+                return response.getPipeInfoList().stream()
+                    .noneMatch(pipeInfo -> pipeName.equals(pipeInfo.getId()));
+              } catch (final Exception e) {
+                return false;
+              }
+            });
+  }
+
+  private static SyncConfigNodeIServiceClient getLeaderClientStatic() throws 
Exception {
+    return (SyncConfigNodeIServiceClient) 
EnvFactory.getEnv().getLeaderConfigNodeConnection();
+  }
+
+  private static void awaitLeader() {
+    Awaitility.await()
+        .pollInterval(1, TimeUnit.SECONDS)
+        .atMost(45, TimeUnit.SECONDS)
+        .until(
+            () -> {
+              try (final SyncConfigNodeIServiceClient ignored = 
getLeaderClientStatic()) {
+                return true;
+              } catch (final Exception e) {
+                return false;
+              }
+            });
+  }
+
+  private static void restartDataNode(final DataNodeWrapper dataNodeWrapper) {
+    if (!dataNodeWrapper.isAlive()) {
+      dataNodeWrapper.start();
+    }
+    EnvFactory.getEnv()
+        .ensureNodeStatus(
+            Collections.singletonList(dataNodeWrapper),
+            Collections.singletonList(NodeStatus.Running));
+  }
+
+  private static final class BlackholeServer implements AutoCloseable {
+    private final ServerSocket serverSocket;
+    private final CopyOnWriteArrayList<Socket> acceptedSockets = new 
CopyOnWriteArrayList<>();
+    private final AtomicBoolean closed = new AtomicBoolean();
+    private final AtomicInteger acceptedConnectionCount = new AtomicInteger();
+    private final AtomicReference<IOException> acceptFailure = new 
AtomicReference<>();
+    private final Thread acceptThread;
+
+    private BlackholeServer(final String address, final int port) throws 
Exception {
+      serverSocket = bind(address, port);
+      acceptThread =
+          new Thread(
+              () -> {
+                while (!closed.get()) {
+                  try {
+                    final Socket socket = serverSocket.accept();
+                    acceptedSockets.add(socket);
+                    acceptedConnectionCount.incrementAndGet();
+                  } catch (final IOException e) {
+                    if (!closed.get()) {
+                      acceptFailure.set(e);
+                    }
+                    return;
+                  }
+                }
+              },
+              "iotdb-pipe-blackhole-" + port);
+      acceptThread.setDaemon(true);
+      acceptThread.start();
+    }
+
+    private static ServerSocket bind(final String address, final int port)
+        throws IOException, InterruptedException {
+      IOException lastException = null;
+      final long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);
+      while (System.nanoTime() < deadline) {
+        final ServerSocket candidate = new ServerSocket();
+        try {
+          candidate.setReuseAddress(true);
+          candidate.bind(new InetSocketAddress(address, port));
+          return candidate;
+        } catch (final IOException e) {
+          lastException = e;
+          candidate.close();
+          TimeUnit.MILLISECONDS.sleep(100);
+        }
+      }
+      throw lastException;
+    }
+
+    private int getAcceptedConnectionCount() {
+      Assert.assertNull(acceptFailure.get());
+      return acceptedConnectionCount.get();
+    }
+
+    @Override
+    public void close() {
+      if (!closed.compareAndSet(false, true)) {
+        return;
+      }
+      try {
+        serverSocket.close();
+      } catch (final IOException ignored) {
+        // The socket is already being closed.
+      }
+      for (final Socket socket : acceptedSockets) {
+        try {
+          socket.close();
+        } catch (final IOException ignored) {
+          // The socket is already being closed.
+        }
+      }
+      try {
+        acceptThread.join(TimeUnit.SECONDS.toMillis(1));
+      } catch (final InterruptedException e) {
+        Thread.currentThread().interrupt();
+      }
+    }
+  }
+}
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/pipe/coordinator/task/PipeTaskCoordinator.java
 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/pipe/coordinator/task/PipeTaskCoordinator.java
index 42d61750c9b..8ac1567be88 100644
--- 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/pipe/coordinator/task/PipeTaskCoordinator.java
+++ 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/pipe/coordinator/task/PipeTaskCoordinator.java
@@ -227,6 +227,14 @@ public class PipeTaskCoordinator {
         req.isSetIfExistsCondition() && req.isIfExistsCondition();
     final boolean isTableModel =
         resolveIsTableModel(pipeName, req.isSetIsTableModel(), 
req.isTableModel);
+    if (pipeTaskInfo.isPipeBeingDropped(pipeName, isTableModel)) {
+      return RpcUtils.getStatus(
+          TSStatusCode.PIPE_ERROR,
+          String.format(
+              ConfigNodeMessages
+                  
.EXCEPTION_FAILED_TO_ALTER_PIPE_ARG_THE_PIPE_IS_BEING_DROPPED_919F1E2B,
+              pipeName));
+    }
     if (!pipeTaskInfo.isPipeExisted(pipeName, isTableModel)) {
       return isSetIfExistsCondition
           ? RpcUtils.getStatus(TSStatusCode.SUCCESS_STATUS)
@@ -266,6 +274,14 @@ public class PipeTaskCoordinator {
     final String pipeName = req.getPipeName();
     final boolean isTableModel =
         resolveIsTableModel(pipeName, req.isSetIsTableModel(), 
req.isTableModel);
+    if (pipeTaskInfo.isPipeBeingDropped(pipeName, isTableModel)) {
+      return RpcUtils.getStatus(
+          TSStatusCode.PIPE_ERROR,
+          String.format(
+              ConfigNodeMessages
+                  
.EXCEPTION_FAILED_TO_START_PIPE_ARG_THE_PIPE_IS_BEING_DROPPED_B41F4638,
+              pipeName));
+    }
     if (!pipeTaskInfo.isPipeExisted(pipeName, isTableModel)) {
       return RpcUtils.getStatus(
           TSStatusCode.PIPE_NOT_EXIST_ERROR,
@@ -298,6 +314,14 @@ public class PipeTaskCoordinator {
     final String pipeName = req.getPipeName();
     final boolean isTableModel =
         resolveIsTableModel(pipeName, req.isSetIsTableModel(), 
req.isTableModel);
+    if (pipeTaskInfo.isPipeBeingDropped(pipeName, isTableModel)) {
+      return RpcUtils.getStatus(
+          TSStatusCode.PIPE_ERROR,
+          String.format(
+              ConfigNodeMessages
+                  
.EXCEPTION_FAILED_TO_STOP_PIPE_ARG_THE_PIPE_IS_BEING_DROPPED_37AFB22B,
+              pipeName));
+    }
     if (!pipeTaskInfo.isPipeExisted(pipeName, isTableModel)) {
       return RpcUtils.getStatus(
           TSStatusCode.PIPE_NOT_EXIST_ERROR,
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 36b87ea5ce7..97856711050 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
@@ -536,6 +536,17 @@ public class PipeTaskInfo implements SnapshotProcessor {
     }
   }
 
+  public boolean isPipeBeingDropped(final String pipeName, final boolean 
isTableModel) {
+    acquireReadLock();
+    try {
+      final PipeMeta pipeMeta = pipeMetaKeeper.getPipeMeta(pipeName, 
isTableModel);
+      return pipeMeta != null
+          && 
PipeStatus.PRE_DELETE.equals(pipeMeta.getRuntimeMeta().getStatus().get());
+    } finally {
+      releaseReadLock();
+    }
+  }
+
   private PipeStatus getPipeStatus(final String pipeName) {
     acquireReadLock();
     try {

Reply via email to