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

voonhous pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git


The following commit(s) were added to refs/heads/master by this push:
     new f41e8e3070d9 fix(flink): close CDC image spillable maps on failures 
(#19482)
f41e8e3070d9 is described below

commit f41e8e3070d94c7997eed933ab76ceb250d77e53
Author: Danny Chan <[email protected]>
AuthorDate: Tue Aug 4 22:51:16 2026 +0800

    fix(flink): close CDC image spillable maps on failures (#19482)
    
    * fix(flink): close CDC image spillable maps on failures
    
    * fix(flink): harden CDC cleanup failure paths
    
    * fix(flink): retain CDC images across child splits
    
    * refactor(flink): simplify CDC iterator cleanup
    
    * refactor(common): share close suppression helper
---
 .../apache/hudi/common/util/CloseableUtils.java    |  35 +++++++
 .../hudi/common/util/TestCloseableUtils.java       |  40 ++++++++
 .../function/HoodieCdcSplitReaderFunction.java     |  15 ++-
 .../reader/function/HoodieSplitReaderFunction.java |  11 +--
 .../hudi/table/format/cdc/CdcImageManager.java     |  31 +++++-
 .../hudi/table/format/cdc/CdcInputFormat.java      |  17 +++-
 .../apache/hudi/table/format/cdc/CdcIterators.java |  30 +++---
 .../hudi/table/format/cdc/TestCdcImageManager.java | 108 +++++++++++++++++++++
 .../hudi/table/format/cdc/TestCdcIterators.java    |  36 ++++++-
 9 files changed, 286 insertions(+), 37 deletions(-)

diff --git 
a/hudi-common/src/main/java/org/apache/hudi/common/util/CloseableUtils.java 
b/hudi-common/src/main/java/org/apache/hudi/common/util/CloseableUtils.java
new file mode 100644
index 000000000000..50b6e2e6ee80
--- /dev/null
+++ b/hudi-common/src/main/java/org/apache/hudi/common/util/CloseableUtils.java
@@ -0,0 +1,35 @@
+/*
+ * 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.hudi.common.util;
+
+/** Utility methods for closing resources. */
+public final class CloseableUtils {
+
+  private CloseableUtils() {
+  }
+
+  /** Closes {@code closeable}, attaching any failure to {@code primary} as a 
suppressed exception. */
+  public static void closeSuppressing(AutoCloseable closeable, Throwable 
primary) {
+    try {
+      closeable.close();
+    } catch (Throwable closeError) {
+      primary.addSuppressed(closeError);
+    }
+  }
+}
diff --git 
a/hudi-common/src/test/java/org/apache/hudi/common/util/TestCloseableUtils.java 
b/hudi-common/src/test/java/org/apache/hudi/common/util/TestCloseableUtils.java
new file mode 100644
index 000000000000..f18339047905
--- /dev/null
+++ 
b/hudi-common/src/test/java/org/apache/hudi/common/util/TestCloseableUtils.java
@@ -0,0 +1,40 @@
+/*
+ * 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.hudi.common.util;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+
+class TestCloseableUtils {
+
+  @Test
+  void testCloseSuppressing() {
+    IOException primary = new IOException("primary");
+    IOException closeError = new IOException("close");
+
+    CloseableUtils.closeSuppressing(() -> {
+      throw closeError;
+    }, primary);
+
+    assertArrayEquals(new Throwable[] {closeError}, primary.getSuppressed());
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/reader/function/HoodieCdcSplitReaderFunction.java
 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/reader/function/HoodieCdcSplitReaderFunction.java
index cd4ea053bc7d..4c595fbacd10 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/reader/function/HoodieCdcSplitReaderFunction.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/reader/function/HoodieCdcSplitReaderFunction.java
@@ -66,6 +66,8 @@ import java.util.List;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
+import static org.apache.hudi.common.util.CloseableUtils.closeSuppressing;
+
 /**
  * CDC reader function for source V2. Reads CDC splits ({@link 
HoodieCdcSourceSplit}) and
  * emits change-log {@link RowData} records tagged with the appropriate {@link 
org.apache.flink.types.RowKind}.
@@ -223,10 +225,15 @@ public class HoodieCdcSplitReaderFunction extends 
AbstractSplitReaderFunction {
         String logFilePath = new Path(tablePath, 
fileSplit.getCdcFiles().get(0)).toString();
         MergeOnReadInputSplit split = 
CdcIterators.singleLogFile2Split(tablePath, logFilePath, 
maxCompactionMemoryInBytes);
         ClosableIterator<HoodieRecord<RowData>> recordIterator = 
getFileSliceHoodieRecordIterator(split);
-        return new CdcIterators.DataLogFileIterator(
-            maxCompactionMemoryInBytes, imageManager, fileSplit, tableSchema,
-            tableState.getRequiredRowType(), tableState.getRequiredPositions(),
-            recordIterator, getMetaClient(), getWriteConfig());
+        try {
+          return new CdcIterators.DataLogFileIterator(
+              maxCompactionMemoryInBytes, imageManager, fileSplit, tableSchema,
+              tableState.getRequiredRowType(), 
tableState.getRequiredPositions(),
+              recordIterator, getMetaClient(), getWriteConfig());
+        } catch (IOException | RuntimeException | Error e) {
+          closeSuppressing(recordIterator, e);
+          throw e;
+        }
       }
       case REPLACE_COMMIT: {
         return new CdcIterators.ReplaceCommitIterator(
diff --git 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/reader/function/HoodieSplitReaderFunction.java
 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/reader/function/HoodieSplitReaderFunction.java
index 0da1dc061d55..4af55f78a4bc 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/reader/function/HoodieSplitReaderFunction.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/reader/function/HoodieSplitReaderFunction.java
@@ -44,6 +44,8 @@ import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
 
+import static org.apache.hudi.common.util.CloseableUtils.closeSuppressing;
+
 /**
  * Default reader function implementation for both MOR and COW tables.
  */
@@ -89,15 +91,6 @@ public class HoodieSplitReaderFunction extends 
AbstractSplitReaderFunction {
     }
   }
 
-  /** Closes {@code reader}, attaching any close failure to {@code primary} as 
a suppressed exception. */
-  private static void closeSuppressing(HoodieRecordReader<RowData> reader, 
Throwable primary) {
-    try {
-      reader.close();
-    } catch (Exception closeError) {
-      primary.addSuppressed(closeError);
-    }
-  }
-
   @Override
   protected RowType producedRowType() {
     return HoodieSchemaConverter.convertToRowType(requiredSchema);
diff --git 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/cdc/CdcImageManager.java
 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/cdc/CdcImageManager.java
index 2854498c9bda..726c9b672abc 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/cdc/CdcImageManager.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/cdc/CdcImageManager.java
@@ -45,6 +45,7 @@ import java.util.Map;
 import java.util.TreeMap;
 import java.util.function.Function;
 
+import static org.apache.hudi.common.util.CloseableUtils.closeSuppressing;
 import static 
org.apache.hudi.hadoop.utils.HoodieInputFormatUtils.HOODIE_RECORD_KEY_COL_POS;
 
 /**
@@ -105,13 +106,16 @@ public class CdcImageManager implements AutoCloseable {
         serializer.serialize(row, new BytesArrayOutputView(baos));
         imageRecordsMap.put(recordKey, baos.toByteArray());
       }
+    } catch (IOException | RuntimeException | Error e) {
+      closeSuppressing(imageRecordsMap, e);
+      throw e;
     }
     return imageRecordsMap;
   }
 
   public RowData getImageRecord(
       String recordKey,
-      ExternalSpillableMap<String, byte[]> imageCache,
+      Map<String, byte[]> imageCache,
       RowKind rowKind) {
     byte[] bytes = imageCache.get(recordKey);
     ValidationUtils.checkState(bytes != null,
@@ -127,7 +131,7 @@ public class CdcImageManager implements AutoCloseable {
 
   public void updateImageRecord(
       String recordKey,
-      ExternalSpillableMap<String, byte[]> imageCache,
+      Map<String, byte[]> imageCache,
       RowData row) {
     ByteArrayOutputStream baos = new ByteArrayOutputStream(4096);
     try {
@@ -140,7 +144,7 @@ public class CdcImageManager implements AutoCloseable {
 
   public RowData removeImageRecord(
       String recordKey,
-      ExternalSpillableMap<String, byte[]> imageCache) {
+      Map<String, byte[]> imageCache) {
     byte[] bytes = imageCache.remove(recordKey);
     if (bytes == null) {
       return null;
@@ -154,8 +158,25 @@ public class CdcImageManager implements AutoCloseable {
 
   @Override
   public void close() {
-    cache.values().forEach(ExternalSpillableMap::close);
-    cache.clear();
+    RuntimeException failure = null;
+    try {
+      for (ExternalSpillableMap<String, byte[]> spillableMap : cache.values()) 
{
+        try {
+          spillableMap.close();
+        } catch (RuntimeException e) {
+          if (failure == null) {
+            failure = e;
+          } else {
+            failure.addSuppressed(e);
+          }
+        }
+      }
+    } finally {
+      cache.clear();
+    }
+    if (failure != null) {
+      throw failure;
+    }
   }
 
   // -------------------------------------------------------------------------
diff --git 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/cdc/CdcInputFormat.java
 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/cdc/CdcInputFormat.java
index ff810bb4906c..cdafe8966abd 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/cdc/CdcInputFormat.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/cdc/CdcInputFormat.java
@@ -48,6 +48,8 @@ import java.io.IOException;
 import java.util.List;
 import java.util.function.Function;
 
+import static org.apache.hudi.common.util.CloseableUtils.closeSuppressing;
+
 /**
  * The base InputFormat class to read Hoodie data set as change logs.
  */
@@ -157,11 +159,16 @@ public class CdcInputFormat extends 
MergeOnReadInputFormat {
         String logFilepath = new Path(tablePath, 
fileSplit.getCdcFiles().get(0)).toString();
         MergeOnReadInputSplit split = 
CdcIterators.singleLogFile2Split(tablePath, logFilepath, 
maxCompactionMemoryInBytes);
         ClosableIterator<HoodieRecord<RowData>> recordIterator = 
getSplitRecordIterator(split);
-        return new CdcIterators.DataLogFileIterator(
-            maxCompactionMemoryInBytes, imageManager, fileSplit,
-            HoodieSchema.parse(tableState.getTableSchema()),
-            tableState.getRequiredRowType(), tableState.getRequiredPositions(),
-            recordIterator, metaClient, imageManager.getWriteConfig());
+        try {
+          return new CdcIterators.DataLogFileIterator(
+              maxCompactionMemoryInBytes, imageManager, fileSplit,
+              HoodieSchema.parse(tableState.getTableSchema()),
+              tableState.getRequiredRowType(), 
tableState.getRequiredPositions(),
+              recordIterator, metaClient, imageManager.getWriteConfig());
+        } catch (IOException | RuntimeException | Error e) {
+          closeSuppressing(recordIterator, e);
+          throw e;
+        }
       case REPLACE_COMMIT:
         return new CdcIterators.ReplaceCommitIterator(
             tablePath, tableState.getRequiredRowType(), 
tableState.getRequiredPositions(),
diff --git 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/cdc/CdcIterators.java
 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/cdc/CdcIterators.java
index 725873329bcc..00bd8879039a 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/cdc/CdcIterators.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/cdc/CdcIterators.java
@@ -60,7 +60,6 @@ import org.apache.hudi.hadoop.fs.HadoopFSUtils;
 import org.apache.hudi.storage.HoodieStorage;
 import org.apache.hudi.storage.StoragePath;
 import org.apache.hudi.table.format.FlinkReaderContextFactory;
-import org.apache.hudi.table.format.FormatUtils;
 import org.apache.hudi.table.format.HoodieRowDataFileReader;
 import org.apache.hudi.table.format.InternalSchemaManager;
 import org.apache.hudi.table.format.mor.MergeOnReadInputSplit;
@@ -80,9 +79,11 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
+import static org.apache.hudi.common.util.CloseableUtils.closeSuppressing;
 import static org.apache.hudi.table.format.FormatUtils.buildAvroRecordBySchema;
 
 /**
@@ -146,11 +147,12 @@ public final class CdcIterators {
 
     @Override
     public void close() {
-      if (recordIterator != null) {
-        recordIterator.close();
-      }
-      if (imageManager != null) {
-        imageManager.close();
+      try (CdcImageManager ignored = imageManager) {
+        if (recordIterator != null) {
+          recordIterator.close();
+        }
+      } finally {
+        recordIterator = null;
         imageManager = null;
       }
     }
@@ -253,7 +255,7 @@ public final class CdcIterators {
     private final String[] orderingFields;
     private final TypedProperties props;
 
-    private ExternalSpillableMap<String, byte[]> beforeImages;
+    private Map<String, byte[]> beforeImages;
     private RowData currentImage;
     private RowData sideImage;
 
@@ -287,15 +289,15 @@ public final class CdcIterators {
           metaClient.getTableConfig().getPartialUpdateMode());
       this.logRecordIterator = logRecordIterator;
       this.deleteContext = new DeleteContext(props, 
tableSchema).withReaderSchema(tableSchema);
-      initImages(cdcFileSplit, writeConfig);
+      initImages(cdcFileSplit);
     }
 
-    private void initImages(HoodieCDCFileSplit fileSplit, HoodieWriteConfig 
writeConfig) throws IOException {
+    private void initImages(HoodieCDCFileSplit fileSplit) throws IOException {
       if (fileSplit.getBeforeFileSlice().isPresent() && 
!fileSplit.getBeforeFileSlice().get().isEmpty()) {
         this.beforeImages = imageManager.getOrLoadImages(
             maxCompactionMemoryInBytes, fileSplit.getBeforeFileSlice().get());
       } else {
-        this.beforeImages = FormatUtils.spillableMap(writeConfig, 
maxCompactionMemoryInBytes, getClass().getSimpleName());
+        this.beforeImages = Collections.emptyMap();
       }
     }
 
@@ -348,7 +350,6 @@ public final class CdcIterators {
     @Override
     public void close() {
       logRecordIterator.close();
-      imageManager.close();
     }
 
     @SuppressWarnings("unchecked")
@@ -662,7 +663,12 @@ public final class CdcIterators {
       this.maxCompactionMemoryInBytes = maxCompactionMemoryInBytes;
       this.projection = RowDataProjection.instance(requiredRowType, 
requiredPositions);
       this.imageManager = imageManager;
-      initImages(fileSplit);
+      try {
+        initImages(fileSplit);
+      } catch (IOException | RuntimeException | Error e) {
+        closeSuppressing(this, e);
+        throw e;
+      }
     }
 
     protected void initImages(HoodieCDCFileSplit fileSplit) throws IOException 
{
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/cdc/TestCdcImageManager.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/cdc/TestCdcImageManager.java
index e761a2a7945f..beb4252c822f 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/cdc/TestCdcImageManager.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/cdc/TestCdcImageManager.java
@@ -34,6 +34,8 @@ import org.apache.flink.table.types.logical.RowType;
 import org.apache.flink.table.types.logical.VarCharType;
 import org.apache.flink.types.RowKind;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
 import org.mockito.MockedStatic;
 
 import java.io.ByteArrayOutputStream;
@@ -50,6 +52,7 @@ import static org.junit.jupiter.api.Assertions.assertSame;
 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.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mockStatic;
 import static org.mockito.Mockito.times;
@@ -154,6 +157,111 @@ class TestCdcImageManager {
     }
   }
 
+  private enum LoadFailure {
+    ITERATOR_CREATION,
+    ITERATION
+  }
+
+  @ParameterizedTest
+  @EnumSource(LoadFailure.class)
+  void testLoadClosesImageCacheWhenLoadFails(LoadFailure mode) {
+    HoodieWriteConfig writeConfig = mock(HoodieWriteConfig.class);
+    when(writeConfig.getBasePath()).thenReturn("/table");
+    ExternalSpillableMap<String, byte[]> imageCache = mockImageCache();
+    ClosableIterator<RowData> iterator = mockIterator();
+    RuntimeException failure = new RuntimeException("load failed");
+    when(iterator.hasNext()).thenThrow(failure);
+    CdcImageManager imageManager = new CdcImageManager(
+        rowType("value"), writeConfig,
+        split -> {
+          if (mode == LoadFailure.ITERATOR_CREATION) {
+            throw failure;
+          }
+          return iterator;
+        });
+
+    try (MockedStatic<FormatUtils> mockedFormatUtils = 
mockStatic(FormatUtils.class)) {
+      mockedFormatUtils.when(() -> FormatUtils.spillableMap(
+          writeConfig, 1024L, CdcImageManager.class.getSimpleName()))
+          .thenReturn(imageCache);
+
+      assertSame(failure, assertThrows(
+          RuntimeException.class,
+          () -> imageManager.getOrLoadImages(1024L, fileSlice("001"))));
+      if (mode == LoadFailure.ITERATION) {
+        verify(iterator).close();
+      }
+      verify(imageCache, times(1)).close();
+      imageManager.close();
+      verify(imageCache, times(1)).close();
+    }
+  }
+
+  @Test
+  void testLoadSuppressesImageCacheCloseError() {
+    HoodieWriteConfig writeConfig = mock(HoodieWriteConfig.class);
+    when(writeConfig.getBasePath()).thenReturn("/table");
+    ExternalSpillableMap<String, byte[]> imageCache = mockImageCache();
+    RuntimeException closeFailure = new RuntimeException("close failed");
+    doThrow(closeFailure).when(imageCache).close();
+    RuntimeException failure = new RuntimeException("load failed");
+    CdcImageManager imageManager = new CdcImageManager(
+        rowType("value"), writeConfig,
+        split -> {
+          throw failure;
+        });
+
+    try (MockedStatic<FormatUtils> mockedFormatUtils = 
mockStatic(FormatUtils.class)) {
+      mockedFormatUtils.when(() -> FormatUtils.spillableMap(
+          writeConfig, 1024L, CdcImageManager.class.getSimpleName()))
+          .thenReturn(imageCache);
+
+      assertSame(failure, assertThrows(
+          RuntimeException.class,
+          () -> imageManager.getOrLoadImages(1024L, fileSlice("001"))));
+      assertEquals(1, failure.getSuppressed().length, "close failure must be 
suppressed, not lost");
+      assertSame(closeFailure, failure.getSuppressed()[0]);
+    }
+  }
+
+  @Test
+  void testCloseContinuesAfterFailureAndClearsCache() throws IOException {
+    HoodieWriteConfig writeConfig = mock(HoodieWriteConfig.class);
+    when(writeConfig.getBasePath()).thenReturn("/table");
+    ExternalSpillableMap<String, byte[]> first = mockImageCache();
+    ExternalSpillableMap<String, byte[]> second = mockImageCache();
+    RuntimeException firstFailure = new RuntimeException("first close failed");
+    RuntimeException secondFailure = new RuntimeException("second close 
failed");
+    doThrow(firstFailure).when(first).close();
+    doThrow(secondFailure).when(second).close();
+    CdcImageManager imageManager = new CdcImageManager(
+        rowType("value"), writeConfig,
+        split -> ClosableIterator.wrap(List.<RowData>of().iterator()));
+
+    try (MockedStatic<FormatUtils> mockedFormatUtils = 
mockStatic(FormatUtils.class)) {
+      mockedFormatUtils.when(() -> FormatUtils.spillableMap(
+          writeConfig, 1024L, CdcImageManager.class.getSimpleName()))
+          .thenReturn(first, second);
+      imageManager.getOrLoadImages(1024L, fileSlice("001"));
+      imageManager.getOrLoadImages(1024L, fileSlice("002"));
+
+      assertSame(firstFailure, assertThrows(RuntimeException.class, 
imageManager::close));
+      assertEquals(1, firstFailure.getSuppressed().length);
+      assertSame(secondFailure, firstFailure.getSuppressed()[0]);
+      verify(first, times(1)).close();
+      verify(second, times(1)).close();
+
+      imageManager.close();
+      verify(first, times(1)).close();
+      verify(second, times(1)).close();
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private static ClosableIterator<RowData> mockIterator() {
+    return mock(ClosableIterator.class);
+  }
+
   @SuppressWarnings("unchecked")
   private static ExternalSpillableMap<String, byte[]> mockImageCache() {
     ExternalSpillableMap<String, byte[]> imageCache = 
mock(ExternalSpillableMap.class);
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/cdc/TestCdcIterators.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/cdc/TestCdcIterators.java
index 2144f05c7c6e..b8b6258dcc69 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/cdc/TestCdcIterators.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/format/cdc/TestCdcIterators.java
@@ -46,7 +46,10 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -117,14 +120,43 @@ class TestCdcIterators {
 
     assertTrue(iterator.hasNext());
     assertSame(row, iterator.next());
+    verify(firstIterator).close();
+    verify(imageManager, never()).close();
     assertFalse(iterator.hasNext());
+    verify(secondIterator).close();
+    verify(imageManager, never()).close();
     iterator.close();
 
-    verify(firstIterator).close();
-    verify(secondIterator).close();
     verify(imageManager).close();
   }
 
+  @Test
+  void testCdcFileSplitsIteratorSuppressesImageManagerCloseFailure() {
+    HoodieCDCFileSplit split = new HoodieCDCFileSplit(
+        "001", HoodieCDCInferenceCase.BASE_FILE_INSERT, "first.parquet");
+    ClosableIterator<RowData> recordIterator = mockIterator();
+    when(recordIterator.hasNext()).thenReturn(true);
+    RuntimeException iteratorFailure = new RuntimeException("iterator close 
failed");
+    doThrow(iteratorFailure).when(recordIterator).close();
+    CdcImageManager imageManager = mock(CdcImageManager.class);
+    RuntimeException managerFailure = new RuntimeException("manager close 
failed");
+    doThrow(managerFailure).when(imageManager).close();
+    CdcIterators.CdcFileSplitsIterator iterator =
+        new CdcIterators.CdcFileSplitsIterator(
+            new HoodieCDCFileSplit[] {split}, imageManager, ignored -> 
recordIterator);
+    assertTrue(iterator.hasNext());
+
+    assertSame(iteratorFailure, assertThrows(RuntimeException.class, 
iterator::close));
+    assertEquals(1, iteratorFailure.getSuppressed().length);
+    assertSame(managerFailure, iteratorFailure.getSuppressed()[0]);
+    verify(recordIterator, times(1)).close();
+    verify(imageManager, times(1)).close();
+
+    iterator.close();
+    verify(recordIterator, times(1)).close();
+    verify(imageManager, times(1)).close();
+  }
+
   @Test
   void testReplaceCommitIteratorReadsBeforeSlice() {
     FileSlice beforeSlice = fileSlice();

Reply via email to