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

davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git


The following commit(s) were added to refs/heads/dev by this push:
     new be960cf71e [Fix][Connector-V2] Run Kafka cleanup with connector 
classloader (#11178)
be960cf71e is described below

commit be960cf71e42cf853ef939c6924e7f8e6a5ba11d
Author: Daniel <[email protected]>
AuthorDate: Sun Jul 5 11:10:18 2026 +0800

    [Fix][Connector-V2] Run Kafka cleanup with connector classloader (#11178)
---
 .../seatunnel/kafka/KafkaClientUtils.java          | 48 +++++++++++++++
 .../kafka/sink/KafkaNoTransactionSender.java       |  8 ++-
 .../seatunnel/kafka/sink/KafkaSinkCommitter.java   | 18 +++---
 .../kafka/sink/KafkaTransactionSender.java         | 13 ++--
 .../kafka/source/KafkaPartitionSplitReader.java    |  3 +-
 .../kafka/source/KafkaSourceSplitEnumerator.java   |  3 +-
 .../seatunnel/kafka/KafkaClientUtilsTest.java      | 69 ++++++++++++++++++++++
 7 files changed, 146 insertions(+), 16 deletions(-)

diff --git 
a/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/KafkaClientUtils.java
 
b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/KafkaClientUtils.java
new file mode 100644
index 0000000000..658b6417ae
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/KafkaClientUtils.java
@@ -0,0 +1,48 @@
+/*
+ * 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.seatunnel.connectors.seatunnel.kafka;
+
+import org.apache.seatunnel.common.utils.TemporaryClassLoaderContext;
+
+/** Utility methods for executing Kafka client operations inside the connector 
classloader. */
+public final class KafkaClientUtils {
+
+    private KafkaClientUtils() {}
+
+    /**
+     * Runs a Kafka client action with the connector classloader as the thread 
context classloader.
+     *
+     * <p>Kafka clients can lazily resolve cleanup implementation classes 
while closing producers,
+     * consumers, and admin clients. SeaTunnel may call connector cleanup from 
engine threads whose
+     * context classloader no longer points at the Kafka connector, so cleanup 
must restore the
+     * connector classloader instead of preloading Kafka private classes by 
name.
+     */
+    public static void runWithConnectorClassLoader(KafkaClientAction action) {
+        try (TemporaryClassLoaderContext ignored =
+                
TemporaryClassLoaderContext.of(KafkaClientUtils.class.getClassLoader())) {
+            action.run();
+        }
+    }
+
+    /** Action executed while the Kafka connector classloader is installed. */
+    @FunctionalInterface
+    public interface KafkaClientAction {
+
+        void run();
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/KafkaNoTransactionSender.java
 
b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/KafkaNoTransactionSender.java
index 46811e5fee..aaa5bbb5f1 100644
--- 
a/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/KafkaNoTransactionSender.java
+++ 
b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/KafkaNoTransactionSender.java
@@ -17,6 +17,7 @@
 
 package org.apache.seatunnel.connectors.seatunnel.kafka.sink;
 
+import org.apache.seatunnel.connectors.seatunnel.kafka.KafkaClientUtils;
 import org.apache.seatunnel.connectors.seatunnel.kafka.state.KafkaCommitInfo;
 import org.apache.seatunnel.connectors.seatunnel.kafka.state.KafkaSinkState;
 
@@ -76,7 +77,10 @@ public class KafkaNoTransactionSender<K, V> implements 
KafkaProduceSender<K, V>
 
     @Override
     public void close() {
-        kafkaProducer.flush();
-        kafkaProducer.close();
+        KafkaClientUtils.runWithConnectorClassLoader(
+                () -> {
+                    kafkaProducer.flush();
+                    kafkaProducer.close();
+                });
     }
 }
diff --git 
a/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/KafkaSinkCommitter.java
 
b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/KafkaSinkCommitter.java
index 4be9fba709..da41ce06a5 100644
--- 
a/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/KafkaSinkCommitter.java
+++ 
b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/KafkaSinkCommitter.java
@@ -19,6 +19,7 @@ package org.apache.seatunnel.connectors.seatunnel.kafka.sink;
 
 import org.apache.seatunnel.api.configuration.ReadonlyConfig;
 import org.apache.seatunnel.api.sink.SinkCommitter;
+import org.apache.seatunnel.connectors.seatunnel.kafka.KafkaClientUtils;
 import org.apache.seatunnel.connectors.seatunnel.kafka.state.KafkaCommitInfo;
 
 import org.apache.kafka.clients.producer.KafkaProducer;
@@ -54,10 +55,7 @@ public class KafkaSinkCommitter implements 
SinkCommitter<KafkaCommitInfo> {
             producer.commitTransaction();
             producer.flush();
         }
-        if (this.kafkaProducer != null) {
-            kafkaProducer.close();
-            kafkaProducer = null;
-        }
+        closeProducer();
         return commitInfos;
     }
 
@@ -70,10 +68,7 @@ public class KafkaSinkCommitter implements 
SinkCommitter<KafkaCommitInfo> {
             KafkaProducer<?, ?> producer = getProducer(commitInfo);
             producer.abortTransaction();
         }
-        if (this.kafkaProducer != null) {
-            kafkaProducer.close();
-            kafkaProducer = null;
-        }
+        closeProducer();
     }
 
     private KafkaInternalProducer<?, ?> getProducer(KafkaCommitInfo 
commitInfo) {
@@ -91,4 +86,11 @@ public class KafkaSinkCommitter implements 
SinkCommitter<KafkaCommitInfo> {
                 commitInfo.getProducerId(), commitInfo.getEpoch(), 
commitInfo.isTxnStarted());
         return kafkaProducer;
     }
+
+    private void closeProducer() {
+        if (this.kafkaProducer != null) {
+            KafkaClientUtils.runWithConnectorClassLoader(kafkaProducer::close);
+            kafkaProducer = null;
+        }
+    }
 }
diff --git 
a/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/KafkaTransactionSender.java
 
b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/KafkaTransactionSender.java
index 0d5101260a..f211db4119 100644
--- 
a/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/KafkaTransactionSender.java
+++ 
b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/KafkaTransactionSender.java
@@ -19,6 +19,7 @@ package org.apache.seatunnel.connectors.seatunnel.kafka.sink;
 
 import org.apache.seatunnel.shade.com.google.common.collect.Lists;
 
+import org.apache.seatunnel.connectors.seatunnel.kafka.KafkaClientUtils;
 import org.apache.seatunnel.connectors.seatunnel.kafka.state.KafkaCommitInfo;
 import org.apache.seatunnel.connectors.seatunnel.kafka.state.KafkaSinkState;
 
@@ -125,10 +126,14 @@ public class KafkaTransactionSender<K, V> implements 
KafkaProduceSender<K, V> {
     @Override
     public void close() {
         if (kafkaProducer != null) {
-            kafkaProducer.flush();
-            // kafkaProducer will abort the transaction if you call close() 
without a duration arg
-            // which will cause an exception when Committer commit the 
transaction later.
-            kafkaProducer.close(Duration.ZERO);
+            KafkaClientUtils.runWithConnectorClassLoader(
+                    () -> {
+                        kafkaProducer.flush();
+                        // kafkaProducer will abort the transaction if you 
call close() without a
+                        // duration arg which will cause an exception when 
Committer commit the
+                        // transaction later.
+                        kafkaProducer.close(Duration.ZERO);
+                    });
         }
     }
 
diff --git 
a/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/source/KafkaPartitionSplitReader.java
 
b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/source/KafkaPartitionSplitReader.java
index 8f17be47e3..9609534090 100644
--- 
a/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/source/KafkaPartitionSplitReader.java
+++ 
b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/source/KafkaPartitionSplitReader.java
@@ -25,6 +25,7 @@ import 
org.apache.seatunnel.connectors.seatunnel.common.source.reader.RecordsWit
 import 
org.apache.seatunnel.connectors.seatunnel.common.source.reader.splitreader.SplitReader;
 import 
org.apache.seatunnel.connectors.seatunnel.common.source.reader.splitreader.SplitsAddition;
 import 
org.apache.seatunnel.connectors.seatunnel.common.source.reader.splitreader.SplitsChange;
+import org.apache.seatunnel.connectors.seatunnel.kafka.KafkaClientUtils;
 
 import org.apache.kafka.clients.consumer.ConsumerConfig;
 import org.apache.kafka.clients.consumer.ConsumerRecord;
@@ -297,7 +298,7 @@ public class KafkaPartitionSplitReader
 
     @Override
     public void close() throws Exception {
-        consumer.close();
+        KafkaClientUtils.runWithConnectorClassLoader(consumer::close);
     }
 
     public void notifyCheckpointComplete(
diff --git 
a/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/source/KafkaSourceSplitEnumerator.java
 
b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/source/KafkaSourceSplitEnumerator.java
index 07b7f9ccb4..c4260ce588 100644
--- 
a/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/source/KafkaSourceSplitEnumerator.java
+++ 
b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/source/KafkaSourceSplitEnumerator.java
@@ -22,6 +22,7 @@ import 
org.apache.seatunnel.shade.com.google.common.annotations.VisibleForTestin
 import org.apache.seatunnel.api.source.SourceSplitEnumerator;
 import org.apache.seatunnel.api.table.catalog.TablePath;
 import org.apache.seatunnel.common.config.Common;
+import org.apache.seatunnel.connectors.seatunnel.kafka.KafkaClientUtils;
 import 
org.apache.seatunnel.connectors.seatunnel.kafka.exception.KafkaConnectorErrorCode;
 import 
org.apache.seatunnel.connectors.seatunnel.kafka.exception.KafkaConnectorException;
 import org.apache.seatunnel.connectors.seatunnel.kafka.state.KafkaSourceState;
@@ -253,7 +254,7 @@ public class KafkaSourceSplitEnumerator
     @Override
     public void close() throws IOException {
         if (this.adminClient != null) {
-            adminClient.close();
+            KafkaClientUtils.runWithConnectorClassLoader(adminClient::close);
         }
         if (scheduledFuture != null) {
             scheduledFuture.cancel(false);
diff --git 
a/seatunnel-connectors-v2/connector-kafka/src/test/java/org/apache/seatunnel/connectors/seatunnel/kafka/KafkaClientUtilsTest.java
 
b/seatunnel-connectors-v2/connector-kafka/src/test/java/org/apache/seatunnel/connectors/seatunnel/kafka/KafkaClientUtilsTest.java
new file mode 100644
index 0000000000..78da3301c7
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-kafka/src/test/java/org/apache/seatunnel/connectors/seatunnel/kafka/KafkaClientUtilsTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.seatunnel.connectors.seatunnel.kafka;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+class KafkaClientUtilsTest {
+
+    @Test
+    void shouldRunActionWithConnectorClassLoaderAndRestoreOriginal() {
+        Thread thread = Thread.currentThread();
+        ClassLoader previousClassLoader = thread.getContextClassLoader();
+        ClassLoader originalClassLoader = new ClassLoader(previousClassLoader) 
{};
+        AtomicReference<ClassLoader> actionClassLoader = new 
AtomicReference<>();
+
+        try {
+            thread.setContextClassLoader(originalClassLoader);
+
+            KafkaClientUtils.runWithConnectorClassLoader(
+                    () -> 
actionClassLoader.set(thread.getContextClassLoader()));
+
+            Assertions.assertSame(KafkaClientUtils.class.getClassLoader(), 
actionClassLoader.get());
+            Assertions.assertSame(originalClassLoader, 
thread.getContextClassLoader());
+        } finally {
+            thread.setContextClassLoader(previousClassLoader);
+        }
+    }
+
+    @Test
+    void shouldRestoreOriginalClassLoaderWhenActionFails() {
+        Thread thread = Thread.currentThread();
+        ClassLoader previousClassLoader = thread.getContextClassLoader();
+        ClassLoader originalClassLoader = new ClassLoader(previousClassLoader) 
{};
+
+        try {
+            thread.setContextClassLoader(originalClassLoader);
+
+            Assertions.assertThrows(
+                    IllegalStateException.class,
+                    () ->
+                            KafkaClientUtils.runWithConnectorClassLoader(
+                                    () -> {
+                                        throw new 
IllegalStateException("expected");
+                                    }));
+
+            Assertions.assertSame(originalClassLoader, 
thread.getContextClassLoader());
+        } finally {
+            thread.setContextClassLoader(previousClassLoader);
+        }
+    }
+}

Reply via email to