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

MartijnVisser pushed a commit to branch v4.1
in repository https://gitbox.apache.org/repos/asf/flink-connector-jdbc.git


The following commit(s) were added to refs/heads/v4.1 by this push:
     new f08f23cf [FLINK-39206][jdbc] Give each split reader its own connection 
provider
f08f23cf is described below

commit f08f23cf1a223068720f1486409c7efb72b6b848
Author: Martijn Visser <[email protected]>
AuthorDate: Mon Sep 21 09:18:30 2026 +0200

    [FLINK-39206][jdbc] Give each split reader its own connection provider
    
    JdbcSource handed the same JdbcConnectionProvider to every split reader it
    created, while flink-connector-base builds a split reader per split fetcher 
and
    closes the reader of a finished split while the reader of the next split is
    already reading. A provider holds a single connection and is not thread 
safe,
    so the finished reader closed the running reader's result set and the job
    failed instead of finishing.
    
    Copy the provider per split reader, the way other connectors give each split
    reader its own client. A reader now owns its copy, so it closes the result 
set,
    the connection and the provider together, and the interface says that it is
    copied.
    
    Generated-by: Claude Code (Opus 5)
---
 .../jdbc/core/datastream/source/JdbcSource.java    |  35 +++++--
 .../source/reader/JdbcSourceSplitReader.java       |  14 ++-
 .../connections/JdbcConnectionProvider.java        |   9 +-
 .../core/datastream/source/JdbcSourceTest.java     | 111 +++++++++++++++++++++
 4 files changed, 153 insertions(+), 16 deletions(-)

diff --git 
a/flink-connector-jdbc-core/src/main/java/org/apache/flink/connector/jdbc/core/datastream/source/JdbcSource.java
 
b/flink-connector-jdbc-core/src/main/java/org/apache/flink/connector/jdbc/core/datastream/source/JdbcSource.java
index 427c9669..fafd5b37 100644
--- 
a/flink-connector-jdbc-core/src/main/java/org/apache/flink/connector/jdbc/core/datastream/source/JdbcSource.java
+++ 
b/flink-connector-jdbc-core/src/main/java/org/apache/flink/connector/jdbc/core/datastream/source/JdbcSource.java
@@ -30,6 +30,7 @@ import 
org.apache.flink.api.connector.source.SplitEnumeratorContext;
 import org.apache.flink.api.java.typeutils.ResultTypeQueryable;
 import org.apache.flink.configuration.Configuration;
 import org.apache.flink.connector.base.DeliveryGuarantee;
+import org.apache.flink.connector.base.source.reader.splitreader.SplitReader;
 import 
org.apache.flink.connector.jdbc.core.datastream.source.config.ContinuousUnBoundingSettings;
 import 
org.apache.flink.connector.jdbc.core.datastream.source.enumerator.JdbcSourceEnumStateSerializer;
 import 
org.apache.flink.connector.jdbc.core.datastream.source.enumerator.JdbcSourceEnumerator;
@@ -39,6 +40,7 @@ import 
org.apache.flink.connector.jdbc.core.datastream.source.enumerator.splitte
 import 
org.apache.flink.connector.jdbc.core.datastream.source.enumerator.splitter.SplitterEnumerator;
 import 
org.apache.flink.connector.jdbc.core.datastream.source.reader.JdbcSourceReader;
 import 
org.apache.flink.connector.jdbc.core.datastream.source.reader.JdbcSourceSplitReader;
+import 
org.apache.flink.connector.jdbc.core.datastream.source.reader.RecordAndOffset;
 import 
org.apache.flink.connector.jdbc.core.datastream.source.reader.extractor.ResultExtractor;
 import 
org.apache.flink.connector.jdbc.core.datastream.source.split.JdbcSourceSplit;
 import 
org.apache.flink.connector.jdbc.core.datastream.source.split.JdbcSourceSplitSerializer;
@@ -49,6 +51,7 @@ import org.apache.flink.core.io.SimpleVersionedSerializer;
 import org.apache.flink.streaming.api.lineage.LineageDataset;
 import org.apache.flink.streaming.api.lineage.LineageVertex;
 import org.apache.flink.streaming.api.lineage.LineageVertexProvider;
+import org.apache.flink.util.InstantiationUtil;
 import org.apache.flink.util.Preconditions;
 
 import javax.annotation.Nullable;
@@ -59,6 +62,7 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.function.Supplier;
 
 /** JDBC source. */
 @PublicEvolving
@@ -120,16 +124,27 @@ public class JdbcSource<OUT>
     public SourceReader<OUT, JdbcSourceSplit> createReader(SourceReaderContext 
readerContext)
             throws Exception {
         return new JdbcSourceReader<>(
-                () ->
-                        new JdbcSourceSplitReader<>(
-                                readerContext,
-                                configuration,
-                                typeInformation,
-                                connectionProvider,
-                                deliveryGuarantee,
-                                resultExtractor),
-                configuration,
-                readerContext);
+                splitReaderSupplier(readerContext), configuration, 
readerContext);
+    }
+
+    /**
+     * Supplies the split readers of one {@link JdbcSourceReader}. Every 
reader gets its own
+     * connection provider: a provider holds a single connection and is not 
thread safe, while
+     * flink-connector-base builds a split reader per split fetcher and closes 
the reader of a
+     * finished split while the reader of the next split is already reading. 
Sharing one provider
+     * lets the first close the connection out from under the second.
+     */
+    @VisibleForTesting
+    Supplier<SplitReader<RecordAndOffset<OUT>, JdbcSourceSplit>> 
splitReaderSupplier(
+            SourceReaderContext readerContext) {
+        return () ->
+                new JdbcSourceSplitReader<>(
+                        readerContext,
+                        configuration,
+                        typeInformation,
+                        InstantiationUtil.cloneUnchecked(connectionProvider),
+                        deliveryGuarantee,
+                        resultExtractor);
     }
 
     @Override
diff --git 
a/flink-connector-jdbc-core/src/main/java/org/apache/flink/connector/jdbc/core/datastream/source/reader/JdbcSourceSplitReader.java
 
b/flink-connector-jdbc-core/src/main/java/org/apache/flink/connector/jdbc/core/datastream/source/reader/JdbcSourceSplitReader.java
index 58928c26..ac6218a0 100644
--- 
a/flink-connector-jdbc-core/src/main/java/org/apache/flink/connector/jdbc/core/datastream/source/reader/JdbcSourceSplitReader.java
+++ 
b/flink-connector-jdbc-core/src/main/java/org/apache/flink/connector/jdbc/core/datastream/source/reader/JdbcSourceSplitReader.java
@@ -32,6 +32,7 @@ import 
org.apache.flink.connector.base.source.reader.splitreader.SplitsChange;
 import 
org.apache.flink.connector.jdbc.core.datastream.source.reader.extractor.ResultExtractor;
 import 
org.apache.flink.connector.jdbc.core.datastream.source.split.JdbcSourceSplit;
 import 
org.apache.flink.connector.jdbc.datasource.connections.JdbcConnectionProvider;
+import org.apache.flink.util.IOUtils;
 import org.apache.flink.util.Preconditions;
 
 import org.slf4j.Logger;
@@ -221,12 +222,15 @@ public class JdbcSourceSplitReader<T>
 
     @Override
     public void close() throws Exception {
-        closeResultSetAndStatement();
-        if (connection != null && !connection.isClosed()) {
-            connection.close();
+        try {
+            // The provider is this reader's own copy, so it is closed here 
too: that runs the
+            // teardown of a user-supplied provider, and covers the connection 
if the provider
+            // hands out one it does not track itself.
+            IOUtils.closeAll(this::closeResultSetAndStatement, connection, 
connectionProvider);
+        } finally {
+            connection = null;
+            currentSplit = null;
         }
-        connection = null;
-        currentSplit = null;
     }
 
     @Override
diff --git 
a/flink-connector-jdbc-core/src/main/java/org/apache/flink/connector/jdbc/datasource/connections/JdbcConnectionProvider.java
 
b/flink-connector-jdbc-core/src/main/java/org/apache/flink/connector/jdbc/datasource/connections/JdbcConnectionProvider.java
index 81c6bccb..b43b9e3e 100644
--- 
a/flink-connector-jdbc-core/src/main/java/org/apache/flink/connector/jdbc/datasource/connections/JdbcConnectionProvider.java
+++ 
b/flink-connector-jdbc-core/src/main/java/org/apache/flink/connector/jdbc/datasource/connections/JdbcConnectionProvider.java
@@ -27,7 +27,14 @@ import java.sql.Connection;
 import java.sql.SQLException;
 import java.util.Properties;
 
-/** JDBC connection provider. */
+/**
+ * Provides the connection a JDBC source or sink works on.
+ *
+ * <p>A source copies the provider once per split reader, by Java 
serialization, so an
+ * implementation that wraps a pool or caches credentials gets one independent 
copy per reader
+ * rather than one per subtask. Keep anything that must not be duplicated out 
of the serialized
+ * state.
+ */
 @PublicEvolving
 public interface JdbcConnectionProvider extends Serializable, AutoCloseable {
     /**
diff --git 
a/flink-connector-jdbc-core/src/test/java/org/apache/flink/connector/jdbc/core/datastream/source/JdbcSourceTest.java
 
b/flink-connector-jdbc-core/src/test/java/org/apache/flink/connector/jdbc/core/datastream/source/JdbcSourceTest.java
new file mode 100644
index 00000000..d33faa4d
--- /dev/null
+++ 
b/flink-connector-jdbc-core/src/test/java/org/apache/flink/connector/jdbc/core/datastream/source/JdbcSourceTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.flink.connector.jdbc.core.datastream.source;
+
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.connector.base.source.reader.RecordsWithSplitIds;
+import org.apache.flink.connector.base.source.reader.splitreader.SplitReader;
+import 
org.apache.flink.connector.base.source.reader.splitreader.SplitsAddition;
+import org.apache.flink.connector.jdbc.JdbcDataTestBase;
+import 
org.apache.flink.connector.jdbc.core.datastream.source.reader.JdbcSourceSplitReader;
+import 
org.apache.flink.connector.jdbc.core.datastream.source.reader.RecordAndOffset;
+import 
org.apache.flink.connector.jdbc.core.datastream.source.split.JdbcSourceSplit;
+import org.apache.flink.connector.testutils.source.reader.TestingReaderContext;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.apache.flink.connector.jdbc.JdbcTestFixture.INPUT_TABLE;
+import static org.apache.flink.connector.jdbc.JdbcTestFixture.TEST_DATA;
+import static org.apache.flink.connector.jdbc.JdbcTestFixture.TestEntry;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Test for {@link JdbcSource}. */
+class JdbcSourceTest extends JdbcDataTestBase {
+
+    private static final String SQL = "SELECT id, title, author, price, qty 
FROM " + INPUT_TABLE;
+
+    @Test
+    void testSplitReadersDoNotShareAConnection() throws Exception {
+        // flink-connector-base builds a split reader per split fetcher and 
closes the reader of a
+        // finished split while the reader of the next split is already 
reading. A connection
+        // provider holds a single connection and is not thread safe, so every 
split reader needs
+        // its own: sharing one lets the finished reader close the running 
reader's result set.
+        JdbcSource<TestEntry> source =
+                JdbcSource.<TestEntry>builder()
+                        
.setTypeInformation(TypeInformation.of(TestEntry.class))
+                        .setSql(SQL)
+                        .setDBUrl(getMetadata().getJdbcUrl())
+                        .setDriverName(getMetadata().getDriverClass())
+                        .setResultExtractor(extractor)
+                        // One record per fetch, so the running reader still 
has an open result set
+                        // when the finished one is closed.
+                        .setSplitReaderFetchBatchSize(1)
+                        .build();
+
+        Supplier<SplitReader<RecordAndOffset<TestEntry>, JdbcSourceSplit>> 
supplier =
+                source.splitReaderSupplier(new TestingReaderContext());
+
+        JdbcSourceSplitReader<TestEntry> finishedSplitReader = 
newSplitReader(supplier, "1");
+        JdbcSourceSplitReader<TestEntry> runningSplitReader = 
newSplitReader(supplier, "2");
+
+        // The first fetch on each opens its result set, which is what 
establishes its connection.
+        drain(finishedSplitReader.fetch());
+        List<TestEntry> records = new 
ArrayList<>(drain(runningSplitReader.fetch()));
+
+        finishedSplitReader.close();
+
+        // The finished reader is gone; the running one must still read its 
split to the end.
+        // One record per fetch, so the remaining count is exactly the number 
of fetches left.
+        for (int i = records.size(); i < TEST_DATA.length; i++) {
+            records.addAll(drain(runningSplitReader.fetch()));
+        }
+        runningSplitReader.close();
+        assertThat(records).containsExactlyInAnyOrder(TEST_DATA);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static JdbcSourceSplitReader<TestEntry> newSplitReader(
+            Supplier<SplitReader<RecordAndOffset<TestEntry>, JdbcSourceSplit>> 
supplier,
+            String splitId) {
+        JdbcSourceSplitReader<TestEntry> splitReader =
+                (JdbcSourceSplitReader<TestEntry>) supplier.get();
+        splitReader.handleSplitsChanges(
+                new SplitsAddition<>(
+                        Collections.singletonList(new JdbcSourceSplit(splitId, 
SQL, null, null))));
+        return splitReader;
+    }
+
+    private static List<TestEntry> drain(
+            RecordsWithSplitIds<RecordAndOffset<TestEntry>> fetchedRecords) {
+        List<TestEntry> records = new ArrayList<>();
+        while (fetchedRecords.nextSplit() != null) {
+            RecordAndOffset<TestEntry> recordAndOffset = 
fetchedRecords.nextRecordFromSplit();
+            while (recordAndOffset != null) {
+                records.add(recordAndOffset.getRecord());
+                recordAndOffset = fetchedRecords.nextRecordFromSplit();
+            }
+        }
+        return records;
+    }
+}

Reply via email to