This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new bda726b82f [flink] Fix source split reader release reliability (#8183)
bda726b82f is described below
commit bda726b82f8ceba4238dc01ffebbc8dec35c8339
Author: QuakeWang <[email protected]>
AuthorDate: Sat Jun 20 18:42:27 2026 +0800
[flink] Fix source split reader release reliability (#8183)
The source split readers keep a single pooled iterator and may also hold
`currentFirstBatch` after seeking past restored records.
The old design had two reliability gaps:
- `close()` only closed the lazy record reader, so an already fetched
`currentFirstBatch` could be left unreleased.
- `releaseBatch()` returned the pooled iterator only after the
underlying batch release succeeded. If `iterator.releaseBatch()` threw,
the single-entry pool lost its iterator and later fetches could block
forever.
This PR releases `currentFirstBatch` during close, always returns the
pooled iterator in a `finally` block, and aligns the CDC reader's
no-split fetch path with the common reader by failing clearly instead of
returning without a valid split.
---
.../cdc/source/reader/CDCSourceSplitReader.java | 25 ++-
.../source/reader/CDCSourceSplitReaderTest.java | 195 +++++++++++++++++++++
.../flink/source/FileStoreSourceSplitReader.java | 23 ++-
.../source/FileStoreSourceSplitReaderTest.java | 174 ++++++++++++++++++
4 files changed, 406 insertions(+), 11 deletions(-)
diff --git
a/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/pipeline/cdc/source/reader/CDCSourceSplitReader.java
b/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/pipeline/cdc/source/reader/CDCSourceSplitReader.java
index 9aeb48567d..59d3f5799c 100644
---
a/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/pipeline/cdc/source/reader/CDCSourceSplitReader.java
+++
b/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/pipeline/cdc/source/reader/CDCSourceSplitReader.java
@@ -185,9 +185,19 @@ public class CDCSourceSplitReader
@Override
public void close() throws Exception {
currentSchemaChangeEvents.clear();
- if (currentReader != null) {
- if (currentReader.lazyRecordReader != null) {
- currentReader.lazyRecordReader.close();
+ try {
+ if (currentFirstBatch != null) {
+ try {
+ currentFirstBatch.releaseBatch();
+ } finally {
+ currentFirstBatch = null;
+ }
+ }
+ } finally {
+ if (currentReader != null) {
+ if (currentReader.lazyRecordReader != null) {
+ currentReader.lazyRecordReader.close();
+ }
}
}
}
@@ -199,7 +209,7 @@ public class CDCSourceSplitReader
final TableAwareFileStoreSourceSplit nextSplit = splits.poll();
if (nextSplit == null) {
- return;
+ throw new IOException("Cannot fetch from another split - no split
remaining");
}
// update metric when split changes
@@ -320,8 +330,11 @@ public class CDCSourceSplitReader
@Override
public void releaseBatch() {
- this.iterator.releaseBatch();
- pool.recycler().recycle(this);
+ try {
+ this.iterator.releaseBatch();
+ } finally {
+ pool.recycler().recycle(this);
+ }
}
}
diff --git
a/paimon-flink/paimon-flink-cdc/src/test/java/org/apache/paimon/flink/pipeline/cdc/source/reader/CDCSourceSplitReaderTest.java
b/paimon-flink/paimon-flink-cdc/src/test/java/org/apache/paimon/flink/pipeline/cdc/source/reader/CDCSourceSplitReaderTest.java
index 82199811b5..8461ed2a66 100644
---
a/paimon-flink/paimon-flink-cdc/src/test/java/org/apache/paimon/flink/pipeline/cdc/source/reader/CDCSourceSplitReaderTest.java
+++
b/paimon-flink/paimon-flink-cdc/src/test/java/org/apache/paimon/flink/pipeline/cdc/source/reader/CDCSourceSplitReaderTest.java
@@ -26,12 +26,14 @@ import org.apache.paimon.catalog.Identifier;
import org.apache.paimon.data.BinaryRow;
import org.apache.paimon.data.GenericRow;
import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.disk.IOManager;
import org.apache.paimon.flink.pipeline.cdc.source.CDCSource;
import
org.apache.paimon.flink.pipeline.cdc.source.TableAwareFileStoreSourceSplit;
import
org.apache.paimon.flink.source.FileStoreSourceReaderTest.DummyMetricGroup;
import org.apache.paimon.flink.source.TestChangelogDataReadWrite;
import org.apache.paimon.flink.source.metrics.FileStoreSourceReaderMetrics;
import org.apache.paimon.io.DataFileMeta;
+import org.apache.paimon.metrics.MetricRegistry;
import org.apache.paimon.options.Options;
import org.apache.paimon.reader.RecordReader;
import org.apache.paimon.schema.Schema;
@@ -69,6 +71,12 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.OptionalLong;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -76,6 +84,7 @@ import static
org.apache.paimon.flink.LogicalTypeConversion.toDataType;
import static
org.apache.paimon.flink.source.FileStoreSourceSplitSerializerTest.newSourceSplit;
import static org.apache.paimon.io.DataFileTestUtils.row;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
/** Test for {@link CDCSourceSplitReader}. */
public class CDCSourceSplitReaderTest {
@@ -444,6 +453,68 @@ public class CDCSourceSplitReaderTest {
reader.close();
}
+ @Test
+ public void testNoSplit() throws Exception {
+ TestChangelogDataReadWrite rw = new
TestChangelogDataReadWrite(tablePath);
+ CDCSourceSplitReader reader = createReader(rw.createReadWithKey());
+ assertThatThrownBy(reader::fetch).hasMessageContaining("no split
remaining");
+ reader.close();
+ }
+
+ @Test
+ public void testRecycleIteratorWhenReleaseBatchFails() throws Exception {
+ CDCSourceSplitReader reader =
+ createReader(
+ new TestingTableRead(
+ new SingleBatchRecordReader(new
FailingReleaseIterator())));
+ try {
+ assignSplit(
+ reader,
+ new TableAwareFileStoreSourceSplit(
+ "id1",
+ new TestingSplit(),
+ 0,
+ Identifier.create(DATABASE, TABLE),
+ 1L,
+ 1L));
+
+ RecordsWithSplitIds<RecordIterator<Event>> records =
reader.fetch();
+ assertThatThrownBy(records::recycle).hasMessageContaining("release
failed");
+
+ RecordsWithSplitIds<RecordIterator<Event>> finishedRecords =
+ fetchWithoutWaitingForPool(reader);
+
assertThat(finishedRecords.finishedSplits()).isEqualTo(Collections.singleton("id1"));
+ } finally {
+ reader.close();
+ }
+ }
+
+ @Test
+ public void testCloseReleasesCurrentFirstBatch() throws Exception {
+ TrackingRecordIterator iterator = new TrackingRecordIterator();
+ CDCSourceSplitReader reader =
+ createReader(new TestingTableRead(new
SingleBatchRecordReader(iterator)));
+ try {
+ assignSplit(
+ reader,
+ new TableAwareFileStoreSourceSplit(
+ "id1",
+ new TestingSplit(),
+ 1,
+ Identifier.create(DATABASE, TABLE),
+ 1L,
+ 1L));
+ reader.wakeUp();
+
+ RecordsWithSplitIds<RecordIterator<Event>> records =
reader.fetch();
+ assertThat(records.finishedSplits()).isEmpty();
+ assertThat(records.nextSplit()).isNull();
+ } finally {
+ reader.close();
+ }
+ assertThat(iterator.released()).isTrue();
+ }
+
@Test
public void testPauseOrResumeSplits() throws Exception {
TestChangelogDataReadWrite rw = new
TestChangelogDataReadWrite(tablePath);
@@ -622,6 +693,22 @@ public class CDCSourceSplitReaderTest {
reader.handleSplitsChanges(splitsChange);
}
+ private RecordsWithSplitIds<RecordIterator<Event>>
fetchWithoutWaitingForPool(
+ CDCSourceSplitReader reader) throws Exception {
+ ExecutorService executorService = Executors.newSingleThreadExecutor();
+ Future<RecordsWithSplitIds<RecordIterator<Event>>> future =
+ executorService.submit(reader::fetch);
+ try {
+ return future.get(5, TimeUnit.SECONDS);
+ } catch (TimeoutException e) {
+ reader.wakeUp();
+ future.get(5, TimeUnit.SECONDS);
+ throw new AssertionError("Timed out waiting for split reader
fetch.", e);
+ } finally {
+ executorService.shutdownNow();
+ }
+ }
+
public static TableAwareFileStoreSourceSplit newSourceSplit(
String id, BinaryRow partition, int bucket, List<DataFileMeta>
files) {
return newSourceSplit(id, partition, bucket, files, false, 0);
@@ -732,4 +819,112 @@ public class CDCSourceSplitReaderTest {
return schemaChangeEvents;
}
}
+
+ private static class TestingTableRead implements TableRead {
+
+ private final RecordReader<InternalRow> recordReader;
+
+ private TestingTableRead(RecordReader<InternalRow> recordReader) {
+ this.recordReader = recordReader;
+ }
+
+ @Override
+ public TableRead withMetricRegistry(MetricRegistry registry) {
+ return this;
+ }
+
+ @Override
+ public TableRead executeFilter() {
+ return this;
+ }
+
+ @Override
+ public TableRead withIOManager(IOManager ioManager) {
+ return this;
+ }
+
+ @Override
+ public RecordReader<InternalRow> createReader(Split split) {
+ return recordReader;
+ }
+ }
+
+ private static class SingleBatchRecordReader implements
RecordReader<InternalRow> {
+
+ private final RecordReader.RecordIterator<InternalRow> iterator;
+ private boolean returned;
+
+ private
SingleBatchRecordReader(RecordReader.RecordIterator<InternalRow> iterator) {
+ this.iterator = iterator;
+ }
+
+ @Nullable
+ @Override
+ public RecordReader.RecordIterator<InternalRow> readBatch() {
+ if (returned) {
+ return null;
+ }
+
+ returned = true;
+ return iterator;
+ }
+
+ @Override
+ public void close() {}
+ }
+
+ private static class FailingReleaseIterator
+ implements RecordReader.RecordIterator<InternalRow> {
+
+ @Nullable
+ @Override
+ public InternalRow next() {
+ return null;
+ }
+
+ @Override
+ public void releaseBatch() {
+ throw new RuntimeException("release failed");
+ }
+ }
+
+ private static class TrackingRecordIterator
+ implements RecordReader.RecordIterator<InternalRow> {
+
+ private boolean returned;
+ private boolean released;
+
+ @Nullable
+ @Override
+ public InternalRow next() {
+ if (returned) {
+ return null;
+ }
+
+ returned = true;
+ return GenericRow.of(1L);
+ }
+
+ @Override
+ public void releaseBatch() {
+ released = true;
+ }
+
+ private boolean released() {
+ return released;
+ }
+ }
+
+ private static class TestingSplit implements Split {
+
+ @Override
+ public long rowCount() {
+ return 0;
+ }
+
+ @Override
+ public OptionalLong mergedRowCount() {
+ return OptionalLong.empty();
+ }
+ }
}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/FileStoreSourceSplitReader.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/FileStoreSourceSplitReader.java
index b49b9adb94..992867ab69 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/FileStoreSourceSplitReader.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/FileStoreSourceSplitReader.java
@@ -191,9 +191,19 @@ public class FileStoreSourceSplitReader
@Override
public void close() throws Exception {
- if (currentReader != null) {
- if (currentReader.lazyRecordReader != null) {
- currentReader.lazyRecordReader.close();
+ try {
+ if (currentFirstBatch != null) {
+ try {
+ currentFirstBatch.releaseBatch();
+ } finally {
+ currentFirstBatch = null;
+ }
+ }
+ } finally {
+ if (currentReader != null) {
+ if (currentReader.lazyRecordReader != null) {
+ currentReader.lazyRecordReader.close();
+ }
}
}
}
@@ -319,8 +329,11 @@ public class FileStoreSourceSplitReader
@Override
public void releaseBatch() {
- this.iterator.releaseBatch();
- pool.recycler().recycle(this);
+ try {
+ this.iterator.releaseBatch();
+ } finally {
+ pool.recycler().recycle(this);
+ }
}
}
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FileStoreSourceSplitReaderTest.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FileStoreSourceSplitReaderTest.java
index d08bc94a40..135cb38dab 100644
---
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FileStoreSourceSplitReaderTest.java
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FileStoreSourceSplitReaderTest.java
@@ -20,13 +20,18 @@ package org.apache.paimon.flink.source;
import org.apache.paimon.KeyValue;
import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.disk.IOManager;
import
org.apache.paimon.flink.source.FileStoreSourceReaderTest.DummyMetricGroup;
import org.apache.paimon.flink.source.metrics.FileStoreSourceReaderMetrics;
import org.apache.paimon.fs.Path;
import org.apache.paimon.fs.local.LocalFileIO;
import org.apache.paimon.io.DataFileMeta;
+import org.apache.paimon.metrics.MetricRegistry;
+import org.apache.paimon.reader.RecordReader;
import org.apache.paimon.schema.Schema;
import org.apache.paimon.schema.SchemaManager;
+import org.apache.paimon.table.source.Split;
import org.apache.paimon.table.source.TableRead;
import org.apache.paimon.utils.RecordWriter;
@@ -52,6 +57,12 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.OptionalLong;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -341,6 +352,45 @@ public class FileStoreSourceSplitReaderTest {
reader.close();
}
+ @Test
+ public void testRecycleIteratorWhenReleaseBatchFails() throws Exception {
+ FileStoreSourceSplitReader reader =
+ createReader(
+ new TestingTableRead(
+ new SingleBatchRecordReader(new
FailingReleaseIterator())),
+ null);
+ try {
+ assignSplit(reader, new FileStoreSourceSplit("id1", new
TestingSplit()));
+
+ RecordsWithSplitIds<RecordIterator<RowData>> records =
reader.fetch();
+ assertThatThrownBy(records::recycle).hasMessageContaining("release
failed");
+
+ RecordsWithSplitIds<RecordIterator<RowData>> finishedRecords =
+ fetchWithoutWaitingForPool(reader);
+
assertThat(finishedRecords.finishedSplits()).isEqualTo(Collections.singleton("id1"));
+ } finally {
+ reader.close();
+ }
+ }
+
+ @Test
+ public void testCloseReleasesCurrentFirstBatch() throws Exception {
+ TrackingRecordIterator iterator = new TrackingRecordIterator();
+ FileStoreSourceSplitReader reader =
+ createReader(new TestingTableRead(new
SingleBatchRecordReader(iterator)), null);
+ try {
+ assignSplit(reader, new FileStoreSourceSplit("id1", new
TestingSplit(), 1));
+ reader.wakeUp();
+
+ RecordsWithSplitIds<RecordIterator<RowData>> records =
reader.fetch();
+ assertThat(records.finishedSplits()).isEmpty();
+ assertThat(records.nextSplit()).isNull();
+ } finally {
+ reader.close();
+ }
+ assertThat(iterator.released()).isTrue();
+ }
+
@Test
public void testLimit() throws Exception {
TestChangelogDataReadWrite rw = new
TestChangelogDataReadWrite(tempDir.toString());
@@ -494,4 +544,128 @@ public class FileStoreSourceSplitReaderTest {
new SplitsAddition<>(Collections.singletonList(split));
reader.handleSplitsChanges(splitsChange);
}
+
+ private RecordsWithSplitIds<RecordIterator<RowData>>
fetchWithoutWaitingForPool(
+ FileStoreSourceSplitReader reader) throws Exception {
+ ExecutorService executorService = Executors.newSingleThreadExecutor();
+ Future<RecordsWithSplitIds<RecordIterator<RowData>>> future =
+ executorService.submit(reader::fetch);
+ try {
+ return future.get(5, TimeUnit.SECONDS);
+ } catch (TimeoutException e) {
+ reader.wakeUp();
+ future.get(5, TimeUnit.SECONDS);
+ throw new AssertionError("Timed out waiting for split reader
fetch.", e);
+ } finally {
+ executorService.shutdownNow();
+ }
+ }
+
+ private static class TestingTableRead implements TableRead {
+
+ private final RecordReader<InternalRow> recordReader;
+
+ private TestingTableRead(RecordReader<InternalRow> recordReader) {
+ this.recordReader = recordReader;
+ }
+
+ @Override
+ public TableRead withMetricRegistry(MetricRegistry registry) {
+ return this;
+ }
+
+ @Override
+ public TableRead executeFilter() {
+ return this;
+ }
+
+ @Override
+ public TableRead withIOManager(IOManager ioManager) {
+ return this;
+ }
+
+ @Override
+ public RecordReader<InternalRow> createReader(Split split) {
+ return recordReader;
+ }
+ }
+
+ private static class SingleBatchRecordReader implements
RecordReader<InternalRow> {
+
+ private final RecordReader.RecordIterator<InternalRow> iterator;
+ private boolean returned;
+
+ private
SingleBatchRecordReader(RecordReader.RecordIterator<InternalRow> iterator) {
+ this.iterator = iterator;
+ }
+
+ @Nullable
+ @Override
+ public RecordReader.RecordIterator<InternalRow> readBatch() {
+ if (returned) {
+ return null;
+ }
+
+ returned = true;
+ return iterator;
+ }
+
+ @Override
+ public void close() {}
+ }
+
+ private static class FailingReleaseIterator
+ implements RecordReader.RecordIterator<InternalRow> {
+
+ @Nullable
+ @Override
+ public InternalRow next() {
+ return null;
+ }
+
+ @Override
+ public void releaseBatch() {
+ throw new RuntimeException("release failed");
+ }
+ }
+
+ private static class TrackingRecordIterator
+ implements RecordReader.RecordIterator<InternalRow> {
+
+ private boolean returned;
+ private boolean released;
+
+ @Nullable
+ @Override
+ public InternalRow next() {
+ if (returned) {
+ return null;
+ }
+
+ returned = true;
+ return GenericRow.of(1L);
+ }
+
+ @Override
+ public void releaseBatch() {
+ released = true;
+ }
+
+ private boolean released() {
+ return released;
+ }
+ }
+
+ private static class TestingSplit implements Split {
+
+ @Override
+ public long rowCount() {
+ return 0;
+ }
+
+ @Override
+ public OptionalLong mergedRowCount() {
+ return OptionalLong.empty();
+ }
+ }
}