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 02f8d5c0756 [Pipe] Allow startup when a plugin JAR is unavailable 
(#18496) (#18600)
02f8d5c0756 is described below

commit 02f8d5c07567aabb9c5911ffe61354fe7b708c8b
Author: Caideyipi <[email protected]>
AuthorDate: Tue Sep 8 17:13:15 2026 +0800

    [Pipe] Allow startup when a plugin JAR is unavailable (#18496) (#18600)
    
    * fix(pipe): allow startup when plugin jar is unavailable
    
    * fix(pipe): isolate unavailable plugin jars during startup
    
    * Update PipeAgentLauncherTest.java
    
    * test(pipe): move plugin load failure test out of PowerMock
    
    * fix(pipe): address review comments
    
    (cherry picked from commit 8a789d09a6046691b3b6003740c48334a0b79aa4)
---
 .../db/pipe/agent/runtime/PipeAgentLauncher.java   | 172 +++++++++++++++++----
 .../pipe/agent/runtime/PipeAgentLauncherTest.java  | 168 ++++++++++++++++++++
 2 files changed, 314 insertions(+), 26 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/runtime/PipeAgentLauncher.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/runtime/PipeAgentLauncher.java
index 01289109371..c7a669fefbf 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/runtime/PipeAgentLauncher.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/agent/runtime/PipeAgentLauncher.java
@@ -19,7 +19,6 @@
 
 package org.apache.iotdb.db.pipe.agent.runtime;
 
-import org.apache.iotdb.commons.client.exception.ClientManagerException;
 import org.apache.iotdb.commons.exception.StartupException;
 import org.apache.iotdb.commons.pipe.agent.plugin.meta.PipePluginMeta;
 import 
org.apache.iotdb.commons.pipe.agent.plugin.service.PipePluginClassLoaderManager;
@@ -38,14 +37,16 @@ import 
org.apache.iotdb.db.service.ResourcesInformationHolder;
 import org.apache.iotdb.pipe.api.exception.PipeException;
 import org.apache.iotdb.rpc.TSStatusCode;
 
-import org.apache.thrift.TException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 class PipeAgentLauncher {
@@ -69,6 +70,7 @@ class PipeAgentLauncher {
 
     final List<PipePluginMeta> uninstalledOrConflictedPipePluginMetaList =
         
getUninstalledOrConflictedPipePluginMetaList(resourcesInformationHolder);
+    final Set<String> unavailablePipePluginNameSet = new HashSet<>();
     int index = 0;
     while (index < uninstalledOrConflictedPipePluginMetaList.size()) {
       List<PipePluginMeta> curList = new ArrayList<>();
@@ -79,20 +81,26 @@ class PipeAgentLauncher {
         offset++;
       }
       index += offset;
-      fetchAndSavePipePluginJars(curList);
+      unavailablePipePluginNameSet.addAll(fetchAndSavePipePluginJars(curList));
     }
 
     // create instances of pipe plugins and do registration
-    try {
-      for (PipePluginMeta meta : 
resourcesInformationHolder.getPipePluginMetaList()) {
-        if (meta.isBuiltin()) {
-          continue;
-        }
+    for (PipePluginMeta meta : 
resourcesInformationHolder.getPipePluginMetaList()) {
+      if (meta.isBuiltin()) {
+        continue;
+      }
+      if (unavailablePipePluginNameSet.contains(meta.getPluginName())) {
+        continue;
+      }
+      try {
         PipeDataNodeAgent.plugin().doRegister(meta);
+      } catch (Throwable e) {
+        // Ignore a single broken plugin and continue startup.
+        LOGGER.error(
+            "Failure when register pipe plugin {}. Skip this plugin and 
continue startup.",
+            meta.getPluginName(),
+            e);
       }
-    } catch (Exception e) {
-      // Ignore the pipe plugin errors and continue to start
-      LOGGER.warn("Failure when register pipe plugins, will ignore.", e);
     }
   }
 
@@ -132,28 +140,140 @@ class PipeAgentLauncher {
     return pipePluginMetaList;
   }
 
-  private static void fetchAndSavePipePluginJars(List<PipePluginMeta> 
pipePluginMetaList)
-      throws StartupException {
+  static Set<String> fetchAndSavePipePluginJars(List<PipePluginMeta> 
pipePluginMetaList) {
+    if (pipePluginMetaList.isEmpty()) {
+      return Collections.emptySet();
+    }
+
+    final List<String> pluginNameList =
+        
pipePluginMetaList.stream().map(PipePluginMeta::getPluginName).collect(Collectors.toList());
+    final List<String> jarNameList =
+        
pipePluginMetaList.stream().map(PipePluginMeta::getJarName).collect(Collectors.toList());
+    final TGetJarInListResp resp;
+
     try (ConfigNodeClient configNodeClient =
         
ConfigNodeClientManager.getInstance().borrowClient(ConfigNodeInfo.CONFIG_REGION_ID))
 {
-      final List<String> jarNameList =
-          
pipePluginMetaList.stream().map(PipePluginMeta::getJarName).collect(Collectors.toList());
-      final TGetJarInListResp resp =
-          configNodeClient.getPipePluginJar(new TGetJarInListReq(jarNameList));
-      if (resp.getStatus().getCode() == 
TSStatusCode.EXECUTE_STATEMENT_ERROR.getStatusCode()) {
-        throw new StartupException("Failed to get pipe plugin jar from config 
node.");
+      resp = configNodeClient.getPipePluginJar(new 
TGetJarInListReq(jarNameList));
+    } catch (Exception e) {
+      LOGGER.error(
+          "Failed to fetch pipe plugin jars from ConfigNode. Plugins: {}, 
jars: {}, status: {}. "
+              + "Retrying each plugin individually.",
+          pluginNameList,
+          jarNameList,
+          null,
+          e);
+      return fetchAndSavePipePluginJarsIndividually(pipePluginMetaList);
+    }
+
+    if (resp == null
+        || resp.getStatus() == null
+        || resp.getStatus().getCode() != 
TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+      LOGGER.error(
+          "Failed to fetch pipe plugin jars from ConfigNode. Plugins: {}, 
jars: {}, status: {}. "
+              + "Retrying each plugin individually.",
+          pluginNameList,
+          jarNameList,
+          resp == null ? null : resp.getStatus());
+      return fetchAndSavePipePluginJarsIndividually(pipePluginMetaList);
+    }
+
+    final List<ByteBuffer> jarList = resp.getJarList();
+    if (jarList == null || jarList.size() != pipePluginMetaList.size()) {
+      LOGGER.error(
+          "ConfigNode returned {} pipe plugin jars for {} requested plugins. "
+              + "Plugins: {}, jars: {}. Retrying each plugin individually.",
+          jarList == null ? 0 : jarList.size(),
+          pipePluginMetaList.size(),
+          pluginNameList,
+          jarNameList);
+      return fetchAndSavePipePluginJarsIndividually(pipePluginMetaList);
+    }
+
+    return savePipePluginJars(pipePluginMetaList, jarList);
+  }
+
+  private static Set<String> fetchAndSavePipePluginJarsIndividually(
+      List<PipePluginMeta> pipePluginMetaList) {
+    final Set<String> unavailablePipePluginNameSet = new HashSet<>();
+    for (PipePluginMeta pipePluginMeta : pipePluginMetaList) {
+      if (!fetchAndSavePipePluginJarIndividually(pipePluginMeta)) {
+        unavailablePipePluginNameSet.add(pipePluginMeta.getPluginName());
       }
-      final List<ByteBuffer> jarList = resp.getJarList();
-      for (int i = 0; i < pipePluginMetaList.size(); i++) {
+    }
+    return unavailablePipePluginNameSet;
+  }
+
+  private static boolean fetchAndSavePipePluginJarIndividually(PipePluginMeta 
pipePluginMeta) {
+    final String pluginName = pipePluginMeta.getPluginName();
+    final String jarName = pipePluginMeta.getJarName();
+    final TGetJarInListResp resp;
+    try (ConfigNodeClient configNodeClient =
+        
ConfigNodeClientManager.getInstance().borrowClient(ConfigNodeInfo.CONFIG_REGION_ID))
 {
+      resp =
+          configNodeClient.getPipePluginJar(
+              new TGetJarInListReq(Collections.singletonList(jarName)));
+    } catch (Exception e) {
+      LOGGER.error(
+          "Failed to fetch pipe plugin jar {} for pipe plugin {} from 
ConfigNode.",
+          jarName,
+          pluginName,
+          e);
+      return false;
+    }
+
+    if (resp == null
+        || resp.getStatus() == null
+        || resp.getStatus().getCode() != 
TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+      final PipeException exception =
+          new PipeException(
+              String.format(
+                  "Failed to fetch pipe plugin jar from ConfigNode for plugin 
%s (jar %s). "
+                      + "Status: %s.",
+                  pluginName, jarName, resp == null ? null : 
resp.getStatus()));
+      LOGGER.error(exception.getMessage(), exception);
+      return false;
+    }
+
+    final List<ByteBuffer> jarList = resp.getJarList();
+    if (jarList == null || jarList.size() != 1) {
+      final PipeException exception =
+          new PipeException(
+              String.format(
+                  "ConfigNode returned %d jars for pipe plugin %s while one 
was requested.",
+                  jarList == null ? 0 : jarList.size(), pluginName));
+      LOGGER.error(exception.getMessage(), exception);
+      return false;
+    }
+
+    try {
+      PipePluginExecutableManager.getInstance()
+          .savePluginToInstallDir(jarList.get(0), pluginName, jarName);
+      return true;
+    } catch (Exception e) {
+      LOGGER.error("Failed to save jar {} for pipe plugin {}.", jarName, 
pluginName, e);
+      return false;
+    }
+  }
+
+  private static Set<String> savePipePluginJars(
+      List<PipePluginMeta> pipePluginMetaList, List<ByteBuffer> jarList) {
+    final Set<String> unavailablePipePluginNameSet = new HashSet<>();
+    for (int i = 0; i < pipePluginMetaList.size(); i++) {
+      final PipePluginMeta pipePluginMeta = pipePluginMetaList.get(i);
+      try {
         PipePluginExecutableManager.getInstance()
             .savePluginToInstallDir(
-                jarList.get(i),
-                pipePluginMetaList.get(i).getPluginName(),
-                pipePluginMetaList.get(i).getJarName());
+                jarList.get(i), pipePluginMeta.getPluginName(), 
pipePluginMeta.getJarName());
+      } catch (Exception e) {
+        LOGGER.error(
+            "Failed to save jar {} for pipe plugin {}.",
+            pipePluginMeta.getJarName(),
+            pipePluginMeta.getPluginName(),
+            e);
+        unavailablePipePluginNameSet.add(pipePluginMeta.getPluginName());
       }
-    } catch (IOException | TException | ClientManagerException e) {
-      throw new StartupException(e);
     }
+    return unavailablePipePluginNameSet;
   }
 
   public static synchronized void launchPipeTaskAgent() {
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/agent/runtime/PipeAgentLauncherTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/agent/runtime/PipeAgentLauncherTest.java
new file mode 100644
index 00000000000..49526ad51be
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/pipe/agent/runtime/PipeAgentLauncherTest.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.db.pipe.agent.runtime;
+
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.client.IClientManager;
+import org.apache.iotdb.commons.consensus.ConfigRegionId;
+import org.apache.iotdb.commons.pipe.agent.plugin.meta.PipePluginMeta;
+import 
org.apache.iotdb.commons.pipe.agent.plugin.service.PipePluginExecutableManager;
+import org.apache.iotdb.confignode.rpc.thrift.TGetJarInListReq;
+import org.apache.iotdb.confignode.rpc.thrift.TGetJarInListResp;
+import org.apache.iotdb.db.protocol.client.ConfigNodeClient;
+import org.apache.iotdb.db.protocol.client.ConfigNodeClientManager;
+import org.apache.iotdb.db.protocol.client.ConfigNodeInfo;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PowerMockIgnore;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+@PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.xml.*", "org.xml.*", 
"javax.management.*"})
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({ConfigNodeClientManager.class, 
PipePluginExecutableManager.class})
+public class PipeAgentLauncherTest {
+
+  private IClientManager<ConfigRegionId, ConfigNodeClient> 
configNodeClientManager;
+  private ConfigNodeClient configNodeClient;
+  private PipePluginExecutableManager pipePluginExecutableManager;
+
+  @Before
+  public void setUp() throws Exception {
+    configNodeClientManager = Mockito.mock(IClientManager.class);
+    configNodeClient = Mockito.mock(ConfigNodeClient.class);
+    pipePluginExecutableManager = 
Mockito.mock(PipePluginExecutableManager.class);
+
+    PowerMockito.mockStatic(ConfigNodeClientManager.class);
+    PowerMockito.mockStatic(PipePluginExecutableManager.class);
+    
PowerMockito.when(ConfigNodeClientManager.getInstance()).thenReturn(configNodeClientManager);
+    PowerMockito.when(PipePluginExecutableManager.getInstance())
+        .thenReturn(pipePluginExecutableManager);
+    
Mockito.when(configNodeClientManager.borrowClient(ConfigNodeInfo.CONFIG_REGION_ID))
+        .thenReturn(configNodeClient);
+  }
+
+  @Test
+  public void 
testBatchFailureRetriesIndividuallyAndOnlyMarksFailedPluginUnavailable()
+      throws Exception {
+    final PipePluginMeta healthyPlugin = pipePluginMeta("healthy", 
"healthy.jar");
+    final PipePluginMeta missingPlugin = pipePluginMeta("missing", 
"missing.jar");
+    final TGetJarInListResp failedResponse = failureResponse();
+
+    
Mockito.when(configNodeClient.getPipePluginJar(Mockito.any(TGetJarInListReq.class)))
+        .thenAnswer(
+            invocation -> {
+              final List<String> jarNames =
+                  ((TGetJarInListReq) 
invocation.getArgument(0)).getJarNameList();
+              if (jarNames.size() == 2 || 
"missing.jar".equals(jarNames.get(0))) {
+                return failedResponse;
+              }
+              return 
successResponse(Collections.singletonList(ByteBuffer.wrap(new byte[] {1})));
+            });
+
+    final Set<String> unavailablePlugins =
+        
PipeAgentLauncher.fetchAndSavePipePluginJars(Arrays.asList(healthyPlugin, 
missingPlugin));
+
+    Assert.assertEquals(Collections.singleton("MISSING"), unavailablePlugins);
+    Mockito.verify(pipePluginExecutableManager)
+        .savePluginToInstallDir(
+            Mockito.any(ByteBuffer.class), Mockito.eq("HEALTHY"), 
Mockito.eq("healthy.jar"));
+  }
+
+  @Test
+  public void testBatchJarCountMismatchRetriesEachPluginIndividually() throws 
Exception {
+    final PipePluginMeta firstPlugin = pipePluginMeta("first", "first.jar");
+    final PipePluginMeta secondPlugin = pipePluginMeta("second", "second.jar");
+
+    
Mockito.when(configNodeClient.getPipePluginJar(Mockito.any(TGetJarInListReq.class)))
+        
.thenReturn(successResponse(Collections.singletonList(ByteBuffer.wrap(new 
byte[] {1}))));
+
+    final Set<String> unavailablePlugins =
+        
PipeAgentLauncher.fetchAndSavePipePluginJars(Arrays.asList(firstPlugin, 
secondPlugin));
+
+    Assert.assertTrue(unavailablePlugins.isEmpty());
+    final ArgumentCaptor<TGetJarInListReq> requestCaptor =
+        ArgumentCaptor.forClass(TGetJarInListReq.class);
+    Mockito.verify(configNodeClient, 
Mockito.times(3)).getPipePluginJar(requestCaptor.capture());
+    Assert.assertEquals(
+        Arrays.asList("first.jar", "second.jar"),
+        requestCaptor.getAllValues().get(0).getJarNameList());
+    Assert.assertEquals(
+        Collections.singletonList("first.jar"),
+        requestCaptor.getAllValues().get(1).getJarNameList());
+    Assert.assertEquals(
+        Collections.singletonList("second.jar"),
+        requestCaptor.getAllValues().get(2).getJarNameList());
+    Mockito.verify(pipePluginExecutableManager, Mockito.times(2))
+        .savePluginToInstallDir(
+            Mockito.any(ByteBuffer.class), Mockito.anyString(), 
Mockito.anyString());
+  }
+
+  @Test
+  public void testSaveFailureDoesNotBlockSubsequentPlugin() throws Exception {
+    final PipePluginMeta failedPlugin = pipePluginMeta("failed", "failed.jar");
+    final PipePluginMeta healthyPlugin = pipePluginMeta("healthy", 
"healthy.jar");
+    
Mockito.when(configNodeClient.getPipePluginJar(Mockito.any(TGetJarInListReq.class)))
+        .thenReturn(
+            successResponse(
+                Arrays.asList(ByteBuffer.wrap(new byte[] {1}), 
ByteBuffer.wrap(new byte[] {2}))));
+    Mockito.doThrow(new IOException("injected save failure"))
+        .when(pipePluginExecutableManager)
+        .savePluginToInstallDir(
+            Mockito.any(ByteBuffer.class), Mockito.eq("FAILED"), 
Mockito.eq("failed.jar"));
+
+    final Set<String> unavailablePlugins =
+        
PipeAgentLauncher.fetchAndSavePipePluginJars(Arrays.asList(failedPlugin, 
healthyPlugin));
+
+    Assert.assertEquals(Collections.singleton("FAILED"), unavailablePlugins);
+    Mockito.verify(pipePluginExecutableManager)
+        .savePluginToInstallDir(
+            Mockito.any(ByteBuffer.class), Mockito.eq("HEALTHY"), 
Mockito.eq("healthy.jar"));
+  }
+
+  private static PipePluginMeta pipePluginMeta(final String pluginName, final 
String jarName) {
+    return new PipePluginMeta(pluginName, "test.class", false, jarName, 
"test-md5");
+  }
+
+  private static TGetJarInListResp successResponse(final List<ByteBuffer> 
jarList) {
+    return new TGetJarInListResp(
+        new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()), jarList);
+  }
+
+  private static TGetJarInListResp failureResponse() {
+    return new TGetJarInListResp(
+        new TSStatus(TSStatusCode.EXECUTE_STATEMENT_ERROR.getStatusCode()),
+        Collections.emptyList());
+  }
+}

Reply via email to