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

mimaison pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/kafka.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 2310e991c06 KAFKA-20751: Preserve available versions for cached 
connector plugin validation failures (#22712)
2310e991c06 is described below

commit 2310e991c0642ebc150c63c23a352c6d5c756dd8
Author: majialong <[email protected]>
AuthorDate: Wed Jul 8 22:31:52 2026 +0800

    KAFKA-20751: Preserve available versions for cached connector plugin 
validation failures (#22712)
    
    Preserve the available plugin versions stored in cached connector plugin
    validation failures.
    
    This keeps repeated validation of an unavailable
    `connector.plugin.version` consistent with the initial validation
    response and avoids an NPE in the cached failure path.
    
    Reviewers: Mickael Maison <[email protected]>
---
 checkstyle/suppressions.xml                        |  2 +-
 .../kafka/connect/runtime/CachedConnectors.java    |  3 +-
 .../kafka/connect/runtime/AbstractHerderTest.java  | 32 ++++++++++
 .../connect/runtime/CachedConnectorsTest.java      | 74 ++++++++++++++++++++++
 4 files changed, 109 insertions(+), 2 deletions(-)

diff --git a/checkstyle/suppressions.xml b/checkstyle/suppressions.xml
index 4f252572fb2..4c0ec0f8ecd 100644
--- a/checkstyle/suppressions.xml
+++ b/checkstyle/suppressions.xml
@@ -137,7 +137,7 @@
 
     <!-- connect tests-->
     <suppress checks="ClassDataAbstractionCoupling"
-              
files="(DistributedHerder|KafkaBasedLog|WorkerSourceTask)Test.java"/>
+              
files="(AbstractHerder|DistributedHerder|KafkaBasedLog|WorkerSourceTask)Test.java"/>
 
     <suppress checks="ClassFanOutComplexity"
               files="(WorkerSink|WorkerSource|ErrorHandling)TaskTest.java"/>
diff --git 
a/connect/runtime/src/main/java/org/apache/kafka/connect/runtime/CachedConnectors.java
 
b/connect/runtime/src/main/java/org/apache/kafka/connect/runtime/CachedConnectors.java
index ae9fe3b99e0..aa56f2d0387 100644
--- 
a/connect/runtime/src/main/java/org/apache/kafka/connect/runtime/CachedConnectors.java
+++ 
b/connect/runtime/src/main/java/org/apache/kafka/connect/runtime/CachedConnectors.java
@@ -50,7 +50,8 @@ public class CachedConnectors {
 
         String version = range == null ? LATEST_VERSION : range.toString();
         if (invalidVersions.containsKey(connectorName) && 
invalidVersions.get(connectorName).containsKey(version)) {
-            throw new 
VersionedPluginLoadingException(invalidVersions.get(connectorName).get(version).getMessage());
+            VersionedPluginLoadingException exception = 
invalidVersions.get(connectorName).get(version);
+            throw new VersionedPluginLoadingException(exception.getMessage(), 
exception.availableVersions());
         }
     }
 
diff --git 
a/connect/runtime/src/test/java/org/apache/kafka/connect/runtime/AbstractHerderTest.java
 
b/connect/runtime/src/test/java/org/apache/kafka/connect/runtime/AbstractHerderTest.java
index 02caa21812f..d1851138ecd 100644
--- 
a/connect/runtime/src/test/java/org/apache/kafka/connect/runtime/AbstractHerderTest.java
+++ 
b/connect/runtime/src/test/java/org/apache/kafka/connect/runtime/AbstractHerderTest.java
@@ -43,6 +43,7 @@ import org.apache.kafka.connect.runtime.isolation.LoaderSwap;
 import org.apache.kafka.connect.runtime.isolation.PluginDesc;
 import org.apache.kafka.connect.runtime.isolation.PluginType;
 import org.apache.kafka.connect.runtime.isolation.Plugins;
+import 
org.apache.kafka.connect.runtime.isolation.VersionedPluginLoadingException;
 import org.apache.kafka.connect.runtime.rest.entities.ConfigInfo;
 import org.apache.kafka.connect.runtime.rest.entities.ConfigInfos;
 import org.apache.kafka.connect.runtime.rest.entities.ConfigKeyInfo;
@@ -405,6 +406,37 @@ public class AbstractHerderTest {
         verifyValidationIsolation();
     }
 
+    @Test
+    public void 
testRepeatedConfigValidationOfInvalidVersionSurfacesAvailableVersions() {
+        Class<? extends Connector> connectorClass = 
SampleSourceConnector.class;
+        String requestedVersion = "2.0.0";
+        List<String> availableVersions = List.of("1.0.0");
+
+        when(worker.getPlugins()).thenReturn(plugins);
+        when(plugins.newConnector(anyString(), any()))
+            .thenThrow(new VersionedPluginLoadingException("no matching 
version", availableVersions));
+
+        AbstractHerder herder = testHerder();
+
+        Map<String, String> config = new HashMap<>();
+        config.put(ConnectorConfig.CONNECTOR_CLASS_CONFIG, 
connectorClass.getName());
+        config.put(ConnectorConfig.CONNECTOR_VERSION, requestedVersion);
+
+        ConfigInfos first = herder.validateConnectorConfig(config, s -> null, 
false);
+        ConfigInfos second = herder.validateConnectorConfig(config, s -> null, 
false);
+
+        ConfigInfo firstVersionInfo = findInfo(first, 
ConnectorConfig.CONNECTOR_VERSION);
+        assertNotNull(firstVersionInfo);
+        assertEquals(availableVersions, 
firstVersionInfo.configValue().recommendedValues());
+
+        ConfigInfo secondVersionInfo = findInfo(second, 
ConnectorConfig.CONNECTOR_VERSION);
+        assertNotNull(secondVersionInfo);
+        assertEquals(availableVersions, 
secondVersionInfo.configValue().recommendedValues());
+
+        // The second validation comes from the cache, so the loader is only 
invoked once.
+        verify(plugins, times(1)).newConnector(eq(connectorClass.getName()), 
any());
+    }
+
     @Test
     public void testBuildRestartPlanForConnectorAndTasks() {
         RestartRequest restartRequest = new RestartRequest(connectorName, 
false, true);
diff --git 
a/connect/runtime/src/test/java/org/apache/kafka/connect/runtime/CachedConnectorsTest.java
 
b/connect/runtime/src/test/java/org/apache/kafka/connect/runtime/CachedConnectorsTest.java
new file mode 100644
index 00000000000..903eb69d50c
--- /dev/null
+++ 
b/connect/runtime/src/test/java/org/apache/kafka/connect/runtime/CachedConnectorsTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.kafka.connect.runtime;
+
+import org.apache.kafka.connect.runtime.isolation.PluginUtils;
+import org.apache.kafka.connect.runtime.isolation.Plugins;
+import 
org.apache.kafka.connect.runtime.isolation.VersionedPluginLoadingException;
+
+import org.apache.maven.artifact.versioning.VersionRange;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class CachedConnectorsTest {
+
+    @Mock
+    private Plugins plugins;
+
+    @Test
+    public void testCachedInvalidVersionFailurePreservesAvailableVersions() 
throws Exception {
+        String requestedVersion = "2.0.0";
+        String connectorClass = 
"org.apache.kafka.connect.runtime.SomeConnector";
+        List<String> availableVersions = List.of("1.0.0");
+        VersionedPluginLoadingException loadingException =
+            new VersionedPluginLoadingException("no matching version", 
availableVersions);
+        when(plugins.newConnector(anyString(), 
any())).thenThrow(loadingException);
+
+        CachedConnectors cachedConnectors = new CachedConnectors(plugins);
+        VersionRange versionRange = 
PluginUtils.connectorVersionRequirement(requestedVersion);
+
+        VersionedPluginLoadingException firstException = assertThrows(
+            VersionedPluginLoadingException.class,
+            () -> cachedConnectors.getConnector(connectorClass, versionRange)
+        );
+        VersionedPluginLoadingException cachedException = assertThrows(
+            VersionedPluginLoadingException.class,
+            () -> cachedConnectors.getConnector(connectorClass, versionRange)
+        );
+
+        // A cached version loading failure should preserve its exception 
details.
+        assertEquals(firstException.getMessage(), 
cachedException.getMessage());
+        assertEquals(availableVersions, cachedException.availableVersions());
+
+        // The second lookup comes from the cache, so the loader is only 
invoked once.
+        verify(plugins, times(1)).newConnector(connectorClass, versionRange);
+    }
+}

Reply via email to