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 9794ece2d55f fix(flink): close bulk insert writer helper during
cleanup (#20242)
9794ece2d55f is described below
commit 9794ece2d55f69fc190f823f99a21a9f3c0fb031
Author: Shuo Cheng <[email protected]>
AuthorDate: Fri Oct 9 17:14:48 2026 +0800
fix(flink): close bulk insert writer helper during cleanup (#20242)
* fix(flink): close bulk insert writer helper during cleanup
* fix(flink): preserve bulk insert cleanup exceptions
---
.../hudi/sink/bulk/BulkInsertWriteFunction.java | 7 +-
.../sink/bulk/TestBulkInsertWriteFunction.java | 175 +++++++++++++++++++++
2 files changed, 178 insertions(+), 4 deletions(-)
diff --git
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bulk/BulkInsertWriteFunction.java
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bulk/BulkInsertWriteFunction.java
index dbcbe5caf971..bcaba63e5d0c 100644
---
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bulk/BulkInsertWriteFunction.java
+++
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bulk/BulkInsertWriteFunction.java
@@ -39,6 +39,7 @@ import
org.apache.flink.runtime.operators.coordination.OperatorEventGateway;
import org.apache.flink.table.data.RowData;
import org.apache.flink.table.types.logical.RowType;
import org.apache.flink.util.Collector;
+import org.apache.flink.util.IOUtils;
import java.io.IOException;
import java.util.List;
@@ -120,10 +121,8 @@ public class BulkInsertWriteFunction<I>
}
@Override
- public void close() {
- if (this.writeClient != null) {
- this.writeClient.close();
- }
+ public void close() throws Exception {
+ IOUtils.closeAll(this.writerHelper, this.writeClient);
}
@Override
diff --git
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/bulk/TestBulkInsertWriteFunction.java
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/bulk/TestBulkInsertWriteFunction.java
new file mode 100644
index 000000000000..8c2b532bc2c0
--- /dev/null
+++
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/bulk/TestBulkInsertWriteFunction.java
@@ -0,0 +1,175 @@
+/*
+ * 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.sink.bulk;
+
+import org.apache.hudi.client.HoodieFlinkWriteClient;
+import org.apache.hudi.client.WriteClientTestUtils;
+import org.apache.hudi.client.WriteStatus;
+import org.apache.hudi.io.storage.row.HoodieRowDataCreateHandle;
+import org.apache.hudi.sink.event.WriteMetadataEvent;
+import org.apache.hudi.table.HoodieFlinkTable;
+import org.apache.hudi.util.FlinkTables;
+import org.apache.hudi.util.StreamerUtil;
+import org.apache.hudi.utils.TestConfigurations;
+import org.apache.hudi.utils.TestData;
+
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.runtime.operators.coordination.OperatorEventGateway;
+import org.apache.flink.table.data.RowData;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.InOrder;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test cases for {@link BulkInsertWriteFunction}.
+ */
+class TestBulkInsertWriteFunction {
+
+ private BulkInsertWriteFunction<RowData> function;
+ private HoodieFlinkWriteClient writeClient;
+ private OperatorEventGateway gateway;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ function = new BulkInsertWriteFunction<>(new Configuration(),
TestConfigurations.ROW_TYPE);
+ writeClient = mock(HoodieFlinkWriteClient.class);
+ gateway = mock(OperatorEventGateway.class);
+ setField("writeClient", writeClient);
+ function.setOperatorEventGateway(gateway);
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testCloseBeforeWriterHelperInitialization(boolean clientCreated) throws
Exception {
+ if (!clientCreated) {
+ setField("writeClient", null);
+ }
+
+ function.close();
+
+ if (clientCreated) {
+ verify(writeClient).close();
+ } else {
+ verifyNoInteractions(writeClient);
+ }
+ verifyNoInteractions(gateway);
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testCloseBeforeEndInput(boolean writeFails) throws Exception {
+ BulkInsertWriterHelper helper = mock(BulkInsertWriterHelper.class);
+ setField("writerHelper", helper);
+ RowData row = TestData.DATA_SET_INSERT.get(0);
+ function.processElement(row, null, null);
+ if (writeFails) {
+ IOException failure = new IOException("write failed");
+ doThrow(failure).when(helper).write(row);
+ assertSame(failure, assertThrows(IOException.class, () ->
function.processElement(row, null, null)));
+ }
+
+ function.close();
+
+ InOrder order = inOrder(helper, writeClient);
+ order.verify(helper).close();
+ order.verify(writeClient).close();
+ verifyNoInteractions(gateway);
+ }
+
+ @ParameterizedTest
+ @CsvSource({"false, false", "true, false", "false, true", "true, true"})
+ void testCloseFailureStillClosesWriteClient(boolean runtimeFailure, boolean
clientCloseFails) throws Exception {
+ BulkInsertWriterHelper helper = mock(BulkInsertWriterHelper.class);
+ setField("writerHelper", helper);
+ Exception failure = runtimeFailure ? new IllegalStateException("close
failed") : new IOException("close failed");
+ doThrow(failure).when(helper).close();
+ RuntimeException clientFailure = new RuntimeException("client close
failed");
+ if (clientCloseFails) {
+ doThrow(clientFailure).when(writeClient).close();
+ }
+
+ Exception thrown = assertThrows(Exception.class, function::close);
+ assertSame(failure, thrown);
+ assertArrayEquals(clientCloseFails ? new Throwable[] {clientFailure} : new
Throwable[0], thrown.getSuppressed());
+
+ InOrder order = inOrder(helper, writeClient);
+ order.verify(helper).close();
+ order.verify(writeClient).close();
+ verifyNoInteractions(gateway);
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testCloseHandlesWithOrWithoutEndInput(boolean endInput, @TempDir File
tempDir) throws Exception {
+ Configuration conf =
TestConfigurations.getDefaultConf(tempDir.getAbsolutePath());
+ StreamerUtil.initTableIfNotExists(conf);
+ HoodieFlinkTable<?> table = FlinkTables.createTable(conf);
+ BulkInsertWriterHelper helper = new BulkInsertWriterHelper(conf, table,
table.getConfig(),
+ WriteClientTestUtils.createNewInstantTime(), 0, 1, 0,
TestConfigurations.ROW_TYPE);
+ HoodieRowDataCreateHandle handle = mock(HoodieRowDataCreateHandle.class);
+ WriteStatus status = new WriteStatus();
+ when(handle.close()).thenReturn(status);
+ helper.handles.put("par1", handle);
+ setField("writerHelper", helper);
+
+ if (endInput) {
+ function.endInput();
+ }
+ function.close();
+
+ verify(handle).close();
+ assertTrue(helper.handles.isEmpty());
+ assertEquals(Collections.singletonList(status),
helper.getWriteStatuses(0));
+ verify(writeClient).close();
+ if (endInput) {
+ verify(gateway).sendEventToCoordinator(any(WriteMetadataEvent.class));
+ verifyNoMoreInteractions(gateway);
+ } else {
+ verifyNoInteractions(gateway);
+ }
+ }
+
+ private void setField(String name, Object value) throws Exception {
+ Field field = BulkInsertWriteFunction.class.getDeclaredField(name);
+ field.setAccessible(true);
+ field.set(function, value);
+ }
+}