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

danny0405 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 3ec54a52d782 fix(common): propagate native log flush failures despite 
ignore failed writes (#20133)
3ec54a52d782 is described below

commit 3ec54a52d782d3af4c94000a5b728e4c40fbe6ec
Author: Shuo Cheng <[email protected]>
AuthorDate: Wed Sep 30 10:51:31 2026 +0800

    fix(common): propagate native log flush failures despite ignore failed 
writes (#20133)
    
    * fix(common): propagate native log flush failures despite ignore failed 
writes
---
 .../org/apache/hudi/io/HoodieAppendHandle.java     |   6 +-
 .../hudi/io/HoodieNativeLogAppendHandle.java       |   2 +-
 .../hudi/io/TestHoodieNativeLogAppendHandle.java   | 122 +++++++++++++++++++++
 3 files changed, 128 insertions(+), 2 deletions(-)

diff --git 
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/io/HoodieAppendHandle.java
 
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/io/HoodieAppendHandle.java
index 8bd2eff5e1ec..56a48a82dd80 100644
--- 
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/io/HoodieAppendHandle.java
+++ 
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/io/HoodieAppendHandle.java
@@ -46,6 +46,7 @@ import org.apache.hudi.common.util.Option;
 import org.apache.hudi.common.util.ReflectionUtils;
 import org.apache.hudi.config.HoodieWriteConfig;
 import org.apache.hudi.exception.ExceptionUtil;
+import org.apache.hudi.exception.HoodieAppendException;
 import org.apache.hudi.exception.HoodieEarlyConflictDetectionException;
 import org.apache.hudi.exception.HoodieException;
 import org.apache.hudi.exception.HoodieUpsertException;
@@ -401,7 +402,10 @@ public abstract class HoodieAppendHandle<T, I, K, O> 
extends HoodieWriteHandle<T
       return true;
     } catch (Exception e) {
       log.error("Error writing record {}", hoodieRecord, e);
-      if (!config.getIgnoreWriteFailed() || ExceptionUtil.isCausedBy(e, 
HoodieEarlyConflictDetectionException.class)) {
+      // A failed log flush affects the entire buffered batch, not just the 
current record.
+      if (!config.getIgnoreWriteFailed()
+          || ExceptionUtil.isCausedBy(e, HoodieAppendException.class)
+          || ExceptionUtil.isCausedBy(e, 
HoodieEarlyConflictDetectionException.class)) {
         closeLogWriterQuietly(e);
         throw new HoodieException(e.getMessage(), e);
       }
diff --git 
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/io/HoodieNativeLogAppendHandle.java
 
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/io/HoodieNativeLogAppendHandle.java
index aae1a6b476dc..b6d76f345dfa 100644
--- 
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/io/HoodieNativeLogAppendHandle.java
+++ 
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/io/HoodieNativeLogAppendHandle.java
@@ -162,7 +162,7 @@ public class HoodieNativeLogAppendHandle<T, I, K, O> 
extends HoodieAppendHandle<
         writer.flushAppend(getUpdatedHeader(header));
         processAppendResults(writer.getLastAppendResults());
       }
-    } catch (IOException e) {
+    } catch (Exception e) {
       throw new HoodieAppendException("Failed while flushing records to native 
log for fileId " + fileId, e);
     }
   }
diff --git 
a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/io/TestHoodieNativeLogAppendHandle.java
 
b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/io/TestHoodieNativeLogAppendHandle.java
index cf68cf4367e8..f700bfe7c8d6 100644
--- 
a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/io/TestHoodieNativeLogAppendHandle.java
+++ 
b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/io/TestHoodieNativeLogAppendHandle.java
@@ -44,6 +44,8 @@ import org.apache.hudi.table.HoodieTable;
 
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.junit.jupiter.params.provider.ValueSource;
 import org.mockito.InOrder;
 import org.mockito.MockedConstruction;
@@ -53,10 +55,12 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import java.util.stream.Stream;
 
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -70,6 +74,7 @@ import static org.mockito.Mockito.mockConstruction;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
 public class TestHoodieNativeLogAppendHandle {
@@ -222,6 +227,123 @@ public class TestHoodieNativeLogAppendHandle {
     }
   }
 
+  private static Stream<Arguments> rolloverFlushFailures() {
+    return Stream.of(new IOException("flush failed"), new 
IllegalStateException("metadata failed"))
+        .flatMap(failure -> Stream.of(false, true).map(deleteRecord -> 
Arguments.of(failure, deleteRecord)));
+  }
+
+  @ParameterizedTest
+  @MethodSource("rolloverFlushFailures")
+  public void testRolloverFlushFailureIsNotIgnored(Exception failure, boolean 
deleteRecord) throws Exception {
+    HoodieWriteConfig config = HoodieWriteConfig.newBuilder()
+        .withProps(config().getProps())
+        .withAllowOperationMetadataField(false)
+        .withWriteIgnoreFailed(true)
+        .build();
+    RuntimeException closeFailure = new IllegalStateException("close failed");
+    try (MockedConstruction<HoodieNativeLogFormatWriter> writers = 
mockConstruction(
+        HoodieNativeLogFormatWriter.class, (writer, context) -> {
+          when(writer.hasPendingWrites()).thenReturn(true);
+          when(writer.canWriteDataFile()).thenReturn(false);
+          when(writer.canWriteDeleteFile()).thenReturn(false);
+          doThrow(failure).when(writer).flushAppend(any());
+          doThrow(closeFailure).when(writer).close();
+        })) {
+      TestableNativeLogAppendHandle handle = new 
TestableNativeLogAppendHandle(config, table(config));
+      handle.createWriter();
+      handle.doInit = false;
+      HoodieRecord record = mock(HoodieRecord.class);
+      HoodieRecord nextRecord = mock(HoodieRecord.class);
+      when(record.getPartitionPath()).thenReturn("partition");
+      when(record.getMetadata()).thenReturn(Option.empty());
+      when(record.isDelete(any(), any())).thenReturn(deleteRecord);
+      when(record.prependMetaFields(any(), any(), any(), 
any())).thenReturn(record);
+      handle.recordItr = Arrays.asList(record, nextRecord).iterator();
+
+      HoodieException exception = assertThrows(HoodieException.class, 
handle::doAppend);
+      assertEquals(HoodieAppendException.class, 
exception.getCause().getClass());
+      assertSame(failure, exception.getCause().getCause());
+      assertArrayEquals(new Throwable[] {closeFailure}, 
exception.getCause().getSuppressed());
+      assertTrue(handle.isClosed());
+      assertNull(handle.getWriter());
+      assertNull(handle.recordItr);
+      assertFalse(handle.writeStatus.hasErrors(), "A flush failure must not be 
recorded as a single-record failure");
+      assertDoesNotThrow(handle::close);
+
+      HoodieNativeLogFormatWriter writer = writers.constructed().get(0);
+      verify(writer).flushAppend(any());
+      verify(writer).close();
+      verify(writer, never()).appendRecord(any(), any());
+      verify(writer, never()).appendDeleteRecord(any(), any());
+      verifyNoInteractions(nextRecord);
+    }
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {false, true})
+  public void testRolloverFlushErrorPropagatesUnchanged(boolean deleteRecord) 
throws Exception {
+    HoodieWriteConfig config = HoodieWriteConfig.newBuilder()
+        .withProps(config().getProps())
+        .withAllowOperationMetadataField(false)
+        .withWriteIgnoreFailed(true)
+        .build();
+    AssertionError failure = new AssertionError("flush failed");
+    try (MockedConstruction<HoodieNativeLogFormatWriter> writers = 
mockConstruction(
+        HoodieNativeLogFormatWriter.class, (writer, context) -> {
+          when(writer.hasPendingWrites()).thenReturn(true);
+          doThrow(failure).when(writer).flushAppend(any());
+        })) {
+      TestableNativeLogAppendHandle handle = new 
TestableNativeLogAppendHandle(config, table(config));
+      handle.createWriter();
+      handle.doInit = false;
+      HoodieRecord record = mock(HoodieRecord.class);
+      HoodieRecord nextRecord = mock(HoodieRecord.class);
+      when(record.getPartitionPath()).thenReturn("partition");
+      when(record.getMetadata()).thenReturn(Option.empty());
+      when(record.isDelete(any(), any())).thenReturn(deleteRecord);
+      when(record.prependMetaFields(any(), any(), any(), 
any())).thenReturn(record);
+      handle.recordItr = Arrays.asList(record, nextRecord).iterator();
+
+      try {
+        // Errors bypass the Exception handlers instead of being wrapped as 
append exceptions.
+        assertSame(failure, assertThrows(AssertionError.class, 
handle::doAppend));
+        assertFalse(handle.writeStatus.hasErrors());
+        HoodieNativeLogFormatWriter writer = writers.constructed().get(0);
+        verify(writer).flushAppend(any());
+        verify(writer, never()).appendRecord(any(), any());
+        verify(writer, never()).appendDeleteRecord(any(), any());
+        verifyNoInteractions(nextRecord);
+      } finally {
+        handle.closeWriter();
+      }
+    }
+  }
+
+  @Test
+  public void testRecordWriteFailureCanStillBeIgnored() throws Exception {
+    HoodieWriteConfig config = config();
+    config.setValue(HoodieWriteConfig.IGNORE_FAILED, "true");
+    try (MockedConstruction<HoodieNativeLogFormatWriter> writers = 
mockConstruction(
+        HoodieNativeLogFormatWriter.class, (writer, context) -> {
+          when(writer.canWriteDataFile()).thenReturn(true);
+          doThrow(new IOException("record write 
failed")).when(writer).appendRecord(any(), any());
+        })) {
+      TestableNativeLogAppendHandle handle = new 
TestableNativeLogAppendHandle(config, table(config));
+      handle.createWriter();
+      handle.doInit = false;
+      HoodieRecord record = mock(HoodieRecord.class);
+      when(record.getPartitionPath()).thenReturn("partition");
+      when(record.getMetadata()).thenReturn(Option.empty());
+      when(record.prependMetaFields(any(), any(), any(), 
any())).thenReturn(record);
+
+      assertFalse(handle.writeRecord(record));
+      assertFalse(handle.isClosed());
+      assertEquals(1, handle.writeStatus.getTotalErrorRecords());
+      verify(writers.constructed().get(0), never()).close();
+      handle.close();
+    }
+  }
+
   @ParameterizedTest
   @ValueSource(booleans = {false, true})
   public void testWriteFailureClosesNativeWriter(boolean useRecordMap) throws 
Exception {

Reply via email to