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 669b4d861229 refactor(flink): share table-service handlers across sink
APIs (#20156)
669b4d861229 is described below
commit 669b4d86122979cd35840d5d88c7bc07b08825aa
Author: Danny Chan <[email protected]>
AuthorDate: Thu Oct 1 19:10:49 2026 +0800
refactor(flink): share table-service handlers across sink APIs (#20156)
* refactor(flink): share table-service handlers across sink APIs
* fix(flink): preserve compaction sink cleanup failures
---
.../java/org/apache/hudi/sink/CleanFunction.java | 13 +-
.../hudi/sink/clustering/ClusteringCommitSink.java | 14 +-
.../hudi/sink/compact/CompactionCommitSink.java | 23 +--
.../handler/DataTableCompactionCommitHandler.java | 15 ++
.../MetadataTableCompactionCommitHandler.java | 15 ++
.../handler/TableServiceHandlerFactory.java | 29 ++--
.../org/apache/hudi/sink/v2/CleanFunctionV2.java | 56 ++-----
.../sink/v2/clustering/ClusteringCommitSinkV2.java | 16 +-
.../sink/v2/compact/CompactionCommitSinkV2.java | 170 ++------------------
.../sink/clustering/TestClusteringCommitSink.java | 37 ++++-
.../sink/compact/CompactionCommitTestSink.java | 27 ++--
.../sink/compact/TestCompactionCommitSink.java | 87 +++++++++++
.../compact/handler/TestDefaultCleanHandler.java | 47 ++++++
.../handler/TestTableServiceHandlerFactory.java | 172 +++++++++++++++++++++
.../apache/hudi/sink/v2/TestCleanFunctionV2.java | 125 +++++++--------
.../v2/clustering/TestClusteringCommitSinkV2.java | 36 ++++-
.../v2/compact/TestCompactionCommitSinkV2.java | 106 ++++++++++++-
17 files changed, 653 insertions(+), 335 deletions(-)
diff --git
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/CleanFunction.java
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/CleanFunction.java
index 50be4ef5b79a..ca53365dc0fd 100644
---
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/CleanFunction.java
+++
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/CleanFunction.java
@@ -27,7 +27,6 @@ import org.apache.hudi.sink.compact.handler.CleanHandler;
import org.apache.hudi.sink.compact.handler.TableServiceHandlerFactory;
import org.apache.hudi.util.FlinkWriteClients;
-import lombok.extern.slf4j.Slf4j;
import org.apache.flink.api.common.state.CheckpointListener;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.runtime.state.FunctionInitializationContext;
@@ -41,14 +40,11 @@ import
org.apache.flink.streaming.api.checkpoint.CheckpointedFunction;
* at a time, a new task can not be scheduled until the last task
finished(fails or normally succeed).
* The cleaning task never expects to throw but only log.
*/
-@Slf4j
public class CleanFunction<T> extends AbstractRichFunctionAdapter
implements SinkFunctionAdapter<T>, CheckpointedFunction,
CheckpointListener {
private final Configuration conf;
- protected HoodieFlinkWriteClient writeClient;
-
private transient Option<CleanHandler> cleanHandlerOpt;
public CleanFunction(Configuration conf) {
@@ -58,9 +54,12 @@ public class CleanFunction<T> extends
AbstractRichFunctionAdapter
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
- this.writeClient = FlinkWriteClients.createWriteClient(conf,
getRuntimeContext());
- this.cleanHandlerOpt = conf.get(FlinkOptions.CLEAN_ASYNC_ENABLED)
- ? Option.of(TableServiceHandlerFactory.createCleanHandler(conf,
writeClient)) : Option.empty();
+ if (conf.get(FlinkOptions.CLEAN_ASYNC_ENABLED)) {
+ HoodieFlinkWriteClient writeClient =
FlinkWriteClients.createWriteClient(conf, getRuntimeContext());
+ this.cleanHandlerOpt =
Option.of(TableServiceHandlerFactory.createCleanHandler(conf, writeClient));
+ } else {
+ this.cleanHandlerOpt = Option.empty();
+ }
this.cleanHandlerOpt.ifPresent(CleanHandler::clean);
}
diff --git
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/clustering/ClusteringCommitSink.java
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/clustering/ClusteringCommitSink.java
index 2bc668d2afa7..d9f16e8755d0 100644
---
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/clustering/ClusteringCommitSink.java
+++
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/clustering/ClusteringCommitSink.java
@@ -20,6 +20,7 @@ package org.apache.hudi.sink.clustering;
import org.apache.hudi.avro.model.HoodieClusteringGroup;
import org.apache.hudi.avro.model.HoodieClusteringPlan;
+import org.apache.hudi.client.HoodieFlinkWriteClient;
import org.apache.hudi.client.WriteStatus;
import org.apache.hudi.common.model.HoodieCommitMetadata;
import org.apache.hudi.common.model.HoodieFileGroupId;
@@ -73,6 +74,8 @@ public class ClusteringCommitSink extends
CleanFunction<ClusteringCommitEvent> {
private transient HoodieFlinkTable<?> table;
+ protected transient HoodieFlinkWriteClient writeClient;
+
/**
* Buffer to collect the event from each clustering task {@code
ClusteringFunction}.
*
@@ -98,9 +101,7 @@ public class ClusteringCommitSink extends
CleanFunction<ClusteringCommitEvent> {
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
- if (writeClient == null) {
- this.writeClient = FlinkWriteClients.createWriteClient(conf,
getRuntimeContext());
- }
+ this.writeClient = FlinkWriteClients.createWriteClient(conf,
getRuntimeContext());
this.commitBuffer = new HashMap<>();
this.clusteringPlanCache = new HashMap<>();
this.table = writeClient.getHoodieTable();
@@ -122,6 +123,13 @@ public class ClusteringCommitSink extends
CleanFunction<ClusteringCommitEvent> {
commitIfNecessary(instant, commitBuffer.get(instant).values());
}
+ @Override
+ public void close() throws Exception {
+ try (HoodieFlinkWriteClient ignored = writeClient) {
+ super.close();
+ }
+ }
+
private long getNumErrorRecords(ClusteringCommitEvent event) {
if (event.getWriteStatuses() == null) {
return -1L;
diff --git
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/compact/CompactionCommitSink.java
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/compact/CompactionCommitSink.java
index 81b753d9ed3e..118c260f0b62 100644
---
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/compact/CompactionCommitSink.java
+++
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/compact/CompactionCommitSink.java
@@ -19,13 +19,12 @@
package org.apache.hudi.sink.compact;
import org.apache.hudi.avro.model.HoodieCompactionPlan;
-import org.apache.hudi.client.WriteStatus;
import org.apache.hudi.sink.CleanFunction;
import org.apache.hudi.sink.compact.handler.CompactionCommitHandler;
import org.apache.hudi.sink.compact.handler.TableServiceHandlerFactory;
-import lombok.extern.slf4j.Slf4j;
import org.apache.flink.configuration.Configuration;
+import org.apache.flink.util.IOUtils;
/**
* Function to check and commit the compaction action.
@@ -38,7 +37,6 @@ import org.apache.flink.configuration.Configuration;
* <p>It also inherits the {@link CleanFunction} cleaning ability. This is
needed because
* the SQL API does not allow multiple sinks in one table sink provider.
*/
-@Slf4j
public class CompactionCommitSink extends CleanFunction<CompactionCommitEvent>
{
/**
@@ -62,28 +60,11 @@ public class CompactionCommitSink extends
CleanFunction<CompactionCommitEvent> {
@Override
public void invoke(CompactionCommitEvent event, Context context) throws
Exception {
- final String instant = event.getInstant();
- if (event.isFailed()
- || (event.getWriteStatuses() != null
- && event.getWriteStatuses().stream().anyMatch(writeStatus ->
writeStatus.getTotalErrorRecords() > 0))) {
- log.warn("Received abnormal CompactionCommitEvent of instant {}, task ID
is {},"
- + " is failed: {}, error record count: {}",
- instant, event.getTaskID(), event.isFailed(),
getNumErrorRecords(event));
- }
compactCommitHandler.commitIfNecessary(event);
}
@Override
public void close() throws Exception {
- compactCommitHandler.close();
- super.close();
- }
-
- private long getNumErrorRecords(CompactionCommitEvent event) {
- if (event.getWriteStatuses() == null) {
- return -1L;
- }
- return event.getWriteStatuses().stream()
- .map(WriteStatus::getTotalErrorRecords).reduce(Long::sum).orElse(0L);
+ IOUtils.closeAll(compactCommitHandler, super::close);
}
}
diff --git
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/compact/handler/DataTableCompactionCommitHandler.java
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/compact/handler/DataTableCompactionCommitHandler.java
index 2b3d7de9437d..717a0b4b29de 100644
---
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/compact/handler/DataTableCompactionCommitHandler.java
+++
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/compact/handler/DataTableCompactionCommitHandler.java
@@ -111,6 +111,13 @@ public class DataTableCompactionCommitHandler implements
CompactionCommitHandler
@Override
public void commitIfNecessary(CompactionCommitEvent event) {
String instant = event.getInstant();
+ if (event.isFailed()
+ || (event.getWriteStatuses() != null
+ && event.getWriteStatuses().stream().anyMatch(writeStatus ->
writeStatus.getTotalErrorRecords() > 0))) {
+ log.warn("Received abnormal CompactionCommitEvent of instant {}, task ID
is {},"
+ + " is failed: {}, error record count: {}",
+ instant, event.getTaskID(), event.isFailed(),
getNumErrorRecords(event));
+ }
commitBuffer.computeIfAbsent(instant, k -> new HashMap<>())
.put(event.getFileId(), event);
@@ -253,6 +260,14 @@ public class DataTableCompactionCommitHandler implements
CompactionCommitHandler
this.compactionPlanCache.remove(instant);
}
+ private long getNumErrorRecords(CompactionCommitEvent event) {
+ if (event.getWriteStatuses() == null) {
+ return -1L;
+ }
+ return event.getWriteStatuses().stream()
+ .map(WriteStatus::getTotalErrorRecords).reduce(Long::sum).orElse(0L);
+ }
+
@Override
public void close() {
writeClient.close();
diff --git
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/compact/handler/MetadataTableCompactionCommitHandler.java
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/compact/handler/MetadataTableCompactionCommitHandler.java
index ced1b0a3a6da..4ff0a9d6228b 100644
---
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/compact/handler/MetadataTableCompactionCommitHandler.java
+++
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/compact/handler/MetadataTableCompactionCommitHandler.java
@@ -58,8 +58,23 @@ import java.util.List;
*/
public class MetadataTableCompactionCommitHandler extends
DataTableCompactionCommitHandler {
+ /** Data client retained for cleanup when no data-table handler owns it. */
+ private final HoodieFlinkWriteClient dataWriteClient;
+
public MetadataTableCompactionCommitHandler(Configuration conf,
HoodieFlinkWriteClient writeClient) {
+ this(conf, writeClient, null);
+ }
+
+ MetadataTableCompactionCommitHandler(Configuration conf,
HoodieFlinkWriteClient writeClient, HoodieFlinkWriteClient dataWriteClient) {
super(conf, writeClient);
+ this.dataWriteClient = dataWriteClient;
+ }
+
+ @Override
+ public void close() {
+ try (HoodieFlinkWriteClient ignored = dataWriteClient) {
+ super.close();
+ }
}
@Override
diff --git
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/compact/handler/TableServiceHandlerFactory.java
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/compact/handler/TableServiceHandlerFactory.java
index 2965866094d1..a7515b966784 100644
---
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/compact/handler/TableServiceHandlerFactory.java
+++
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/compact/handler/TableServiceHandlerFactory.java
@@ -19,6 +19,7 @@
package org.apache.hudi.sink.compact.handler;
import org.apache.hudi.client.HoodieFlinkWriteClient;
+import org.apache.hudi.common.util.CloseableUtils;
import org.apache.hudi.configuration.OptionsResolver;
import org.apache.hudi.util.FlinkWriteClients;
import org.apache.hudi.util.StreamerUtil;
@@ -65,17 +66,25 @@ public final class TableServiceHandlerFactory {
public static CompactionCommitHandler
createCompactionCommitHandler(Configuration conf, RuntimeContext
runtimeContext) {
HoodieFlinkWriteClient writeClient =
FlinkWriteClients.createWriteClient(conf, runtimeContext);
- boolean needsDataCompaction = OptionsResolver.needsAsyncCompaction(conf);
- boolean needsMetadataCompaction =
OptionsResolver.needsAsyncMetadataCompaction(conf);
- if (needsDataCompaction && needsMetadataCompaction) {
- return new CompositeCompactionCommitHandler(
- new DataTableCompactionCommitHandler(conf, writeClient),
- new MetadataTableCompactionCommitHandler(conf,
StreamerUtil.createMetadataWriteClient(writeClient)));
- }
- if (needsMetadataCompaction) {
- return new MetadataTableCompactionCommitHandler(conf,
StreamerUtil.createMetadataWriteClient(writeClient));
+ HoodieFlinkWriteClient metadataWriteClient = null;
+ try {
+ boolean needsDataCompaction = OptionsResolver.needsAsyncCompaction(conf);
+ boolean needsMetadataCompaction =
OptionsResolver.needsAsyncMetadataCompaction(conf);
+ if (needsMetadataCompaction) {
+ metadataWriteClient =
StreamerUtil.createMetadataWriteClient(writeClient);
+ if (needsDataCompaction) {
+ return new CompositeCompactionCommitHandler(
+ new DataTableCompactionCommitHandler(conf, writeClient),
+ new MetadataTableCompactionCommitHandler(conf,
metadataWriteClient));
+ }
+ return new MetadataTableCompactionCommitHandler(conf,
metadataWriteClient, writeClient);
+ }
+ return new DataTableCompactionCommitHandler(conf, writeClient);
+ } catch (RuntimeException | Error failure) {
+ CloseableUtils.closeSuppressing(metadataWriteClient, failure);
+ CloseableUtils.closeSuppressing(writeClient, failure);
+ throw failure;
}
- return new DataTableCompactionCommitHandler(conf, writeClient);
}
public static CleanHandler createCleanHandler(Configuration conf,
HoodieFlinkWriteClient writeClient) {
diff --git
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/v2/CleanFunctionV2.java
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/v2/CleanFunctionV2.java
index 93d7496debb0..69c4f217ef64 100644
---
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/v2/CleanFunctionV2.java
+++
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/v2/CleanFunctionV2.java
@@ -20,11 +20,12 @@ package org.apache.hudi.sink.v2;
import org.apache.hudi.adapter.ProcessFunctionAdapter;
import org.apache.hudi.client.HoodieFlinkWriteClient;
+import org.apache.hudi.common.util.Option;
import org.apache.hudi.configuration.FlinkOptions;
-import org.apache.hudi.sink.utils.NonThrownExecutor;
+import org.apache.hudi.sink.compact.handler.CleanHandler;
+import org.apache.hudi.sink.compact.handler.TableServiceHandlerFactory;
import org.apache.hudi.util.FlinkWriteClients;
-import lombok.extern.slf4j.Slf4j;
import org.apache.flink.api.common.state.CheckpointListener;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.runtime.state.FunctionInitializationContext;
@@ -44,17 +45,12 @@ import org.apache.flink.util.Collector;
* <p>Note: The difference with {@code CleanFunction} is {@code
CleanFunctionV2} extends {@code ProcessFunction},
* while {@code CleanFunction} is a {@code SinkFunction}.
*/
-@Slf4j
public class CleanFunctionV2<T> extends ProcessFunctionAdapter<T, RowData>
implements CheckpointedFunction, CheckpointListener {
private final Configuration conf;
- protected HoodieFlinkWriteClient writeClient;
-
- private NonThrownExecutor executor;
-
- protected volatile boolean isCleaning;
+ private transient Option<CleanHandler> cleanHandlerOpt;
public CleanFunctionV2(Configuration conf) {
this.conf = conf;
@@ -63,45 +59,23 @@ public class CleanFunctionV2<T> extends
ProcessFunctionAdapter<T, RowData>
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
- this.writeClient = FlinkWriteClients.createWriteClient(conf,
getRuntimeContext());
- this.executor =
NonThrownExecutor.builder(log).waitForTasksFinish(true).build();
if (conf.get(FlinkOptions.CLEAN_ASYNC_ENABLED)) {
- executor.execute(() -> {
- this.isCleaning = true;
- try {
- this.writeClient.clean();
- } finally {
- this.isCleaning = false;
- }
- }, "wait for cleaning finish");
+ HoodieFlinkWriteClient writeClient =
FlinkWriteClients.createWriteClient(conf, getRuntimeContext());
+ this.cleanHandlerOpt =
Option.of(TableServiceHandlerFactory.createCleanHandler(conf, writeClient));
+ } else {
+ this.cleanHandlerOpt = Option.empty();
}
+ this.cleanHandlerOpt.ifPresent(CleanHandler::clean);
}
@Override
public void notifyCheckpointComplete(long l) throws Exception {
- if (conf.get(FlinkOptions.CLEAN_ASYNC_ENABLED) && isCleaning) {
- executor.execute(() -> {
- try {
- this.writeClient.waitForCleaningFinish();
- } finally {
- // ensure to switch the isCleaning flag
- this.isCleaning = false;
- }
- }, "wait for cleaning finish");
- }
+ this.cleanHandlerOpt.ifPresent(CleanHandler::waitForCleaningFinish);
}
@Override
public void snapshotState(FunctionSnapshotContext context) throws Exception {
- if (conf.get(FlinkOptions.CLEAN_ASYNC_ENABLED) && !isCleaning) {
- try {
- this.writeClient.startAsyncCleaning();
- this.isCleaning = true;
- } catch (Throwable throwable) {
- // catch the exception to not affect the normal checkpointing
- log.warn("Unable to start async cleaning", throwable);
- }
- }
+ this.cleanHandlerOpt.ifPresent(CleanHandler::startAsyncCleaning);
}
@Override
@@ -111,13 +85,7 @@ public class CleanFunctionV2<T> extends
ProcessFunctionAdapter<T, RowData>
@Override
public void close() throws Exception {
- if (executor != null) {
- executor.close();
- }
-
- if (this.writeClient != null) {
- this.writeClient.close();
- }
+ this.cleanHandlerOpt.ifPresent(CleanHandler::close);
}
@Override
diff --git
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/v2/clustering/ClusteringCommitSinkV2.java
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/v2/clustering/ClusteringCommitSinkV2.java
index 4f31ee877f7a..51148e6085d8 100644
---
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/v2/clustering/ClusteringCommitSinkV2.java
+++
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/v2/clustering/ClusteringCommitSinkV2.java
@@ -20,6 +20,7 @@ package org.apache.hudi.sink.v2.clustering;
import org.apache.hudi.avro.model.HoodieClusteringGroup;
import org.apache.hudi.avro.model.HoodieClusteringPlan;
+import org.apache.hudi.client.HoodieFlinkWriteClient;
import org.apache.hudi.client.WriteStatus;
import org.apache.hudi.common.model.HoodieCommitMetadata;
import org.apache.hudi.common.model.HoodieFileGroupId;
@@ -80,6 +81,8 @@ public class ClusteringCommitSinkV2 extends
CleanFunctionV2<ClusteringCommitEven
private transient HoodieFlinkTable<?> table;
+ protected transient HoodieFlinkWriteClient writeClient;
+
/**
* Buffer to collect the event from each clustering task {@code
ClusteringFunction}.
*
@@ -105,9 +108,7 @@ public class ClusteringCommitSinkV2 extends
CleanFunctionV2<ClusteringCommitEven
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
- if (writeClient == null) {
- this.writeClient = FlinkWriteClients.createWriteClient(conf,
getRuntimeContext());
- }
+ this.writeClient = FlinkWriteClients.createWriteClient(conf,
getRuntimeContext());
this.commitBuffer = new HashMap<>();
this.clusteringPlanCache = new HashMap<>();
this.table = writeClient.getHoodieTable();
@@ -132,6 +133,13 @@ public class ClusteringCommitSinkV2 extends
CleanFunctionV2<ClusteringCommitEven
commitIfNecessary(instant, commitBuffer.get(instant).values());
}
+ @Override
+ public void close() throws Exception {
+ try (HoodieFlinkWriteClient ignored = writeClient) {
+ super.close();
+ }
+ }
+
private long getNumErrorRecords(ClusteringCommitEvent event) {
if (event.getWriteStatuses() == null) {
return -1L;
@@ -230,7 +238,7 @@ public class ClusteringCommitSinkV2 extends
CleanFunctionV2<ClusteringCommitEven
clusteringMetrics.updateCommitMetrics(instant,
writeMetadata.getCommitMetadata().get());
// whether to clean up the input base parquet files used for clustering
- if (!conf.get(FlinkOptions.CLEAN_ASYNC_ENABLED) && !isCleaning) {
+ if (!conf.get(FlinkOptions.CLEAN_ASYNC_ENABLED)) {
log.info("Running inline clean");
this.writeClient.clean();
}
diff --git
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/v2/compact/CompactionCommitSinkV2.java
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/v2/compact/CompactionCommitSinkV2.java
index 7e81112bb0b6..a8157e0e7da8 100644
---
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/v2/compact/CompactionCommitSinkV2.java
+++
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/v2/compact/CompactionCommitSinkV2.java
@@ -19,33 +19,16 @@
package org.apache.hudi.sink.v2.compact;
import org.apache.hudi.avro.model.HoodieCompactionPlan;
-import org.apache.hudi.client.WriteStatus;
-import org.apache.hudi.common.data.HoodieListData;
-import org.apache.hudi.common.model.HoodieCommitMetadata;
-import org.apache.hudi.common.util.CompactionUtils;
-import org.apache.hudi.configuration.FlinkOptions;
-import org.apache.hudi.exception.HoodieException;
-import org.apache.hudi.metrics.FlinkCompactionMetrics;
import org.apache.hudi.sink.compact.CompactionCommitEvent;
+import org.apache.hudi.sink.compact.handler.CompactionCommitHandler;
+import org.apache.hudi.sink.compact.handler.TableServiceHandlerFactory;
import org.apache.hudi.sink.v2.CleanFunctionV2;
-import org.apache.hudi.table.HoodieFlinkTable;
-import org.apache.hudi.table.action.compact.CompactHelpers;
-import org.apache.hudi.util.CompactionUtil;
-import org.apache.hudi.util.FlinkWriteClients;
-import lombok.extern.slf4j.Slf4j;
import org.apache.flink.configuration.Configuration;
-import org.apache.flink.metrics.MetricGroup;
import org.apache.flink.streaming.api.functions.ProcessFunction;
import org.apache.flink.table.data.RowData;
import org.apache.flink.util.Collector;
-
-import java.io.IOException;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
+import org.apache.flink.util.IOUtils;
/**
* Function to check and commit the compaction action.
@@ -61,7 +44,6 @@ import java.util.stream.Collectors;
* <p>Note: The difference with {@code CompactionCommitSink} is {@code
CompactionCommitSinkV2}
* extends {@code ProcessFunction}, while {@code CompactionCommitSink} is a
{@code SinkFunction}.
*/
-@Slf4j
public class CompactionCommitSinkV2 extends
CleanFunctionV2<CompactionCommitEvent> {
/**
@@ -69,30 +51,7 @@ public class CompactionCommitSinkV2 extends
CleanFunctionV2<CompactionCommitEven
*/
private final Configuration conf;
- /**
- * Buffer to collect the event from each compact task {@code
CompactFunction}.
- *
- * <p>Stores the mapping of instant_time -> file_id -> event. Use a map to
collect the
- * events because the rolling back of intermediate compaction tasks
generates corrupt
- * events.
- */
- private transient Map<String, Map<String, CompactionCommitEvent>>
commitBuffer;
-
- /**
- * Cache to store compaction plan for each instant.
- * Stores the mapping of instant_time -> compactionPlan.
- */
- private transient Map<String, HoodieCompactionPlan> compactionPlanCache;
-
- /**
- * The hoodie table.
- */
- private transient HoodieFlinkTable<?> table;
-
- /**
- * Compaction metrics.
- */
- private transient FlinkCompactionMetrics compactionMetrics;
+ private transient CompactionCommitHandler compactCommitHandler;
public CompactionCommitSinkV2(Configuration conf) {
super(conf);
@@ -102,13 +61,8 @@ public class CompactionCommitSinkV2 extends
CleanFunctionV2<CompactionCommitEven
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
- if (writeClient == null) {
- this.writeClient = FlinkWriteClients.createWriteClient(conf,
getRuntimeContext());
- }
- this.commitBuffer = new HashMap<>();
- this.compactionPlanCache = new HashMap<>();
- this.table = this.writeClient.getHoodieTable();
- registerMetrics();
+ this.compactCommitHandler =
TableServiceHandlerFactory.createCompactionCommitHandler(conf,
getRuntimeContext());
+
this.compactCommitHandler.registerMetrics(getRuntimeContext().getMetricGroup());
}
@Override
@@ -116,115 +70,11 @@ public class CompactionCommitSinkV2 extends
CleanFunctionV2<CompactionCommitEven
CompactionCommitEvent event,
ProcessFunction<CompactionCommitEvent, RowData>.Context context,
Collector<RowData> collector) throws Exception {
- final String instant = event.getInstant();
- if (event.isFailed()
- || (event.getWriteStatuses() != null
- && event.getWriteStatuses().stream().anyMatch(writeStatus ->
writeStatus.getTotalErrorRecords() > 0))) {
- log.warn("Received abnormal CompactionCommitEvent of instant {}, task ID
is {},"
- + " is failed: {}, error record count: {}",
- instant, event.getTaskID(), event.isFailed(),
getNumErrorRecords(event));
- }
- commitBuffer.computeIfAbsent(instant, k -> new HashMap<>())
- .put(event.getFileId(), event);
- commitIfNecessary(instant, commitBuffer.get(instant).values());
- }
-
- private long getNumErrorRecords(CompactionCommitEvent event) {
- if (event.getWriteStatuses() == null) {
- return -1L;
- }
- return event.getWriteStatuses().stream()
- .map(WriteStatus::getTotalErrorRecords).reduce(Long::sum).orElse(0L);
- }
-
- /**
- * Condition to commit: the commit buffer has equal size with the compaction
plan operations
- * and all the compact commit event {@link CompactionCommitEvent} has the
same compaction instant time.
- *
- * @param instant Compaction commit instant time
- * @param events Commit events ever received for the instant
- */
- private void commitIfNecessary(String instant,
Collection<CompactionCommitEvent> events) throws IOException {
- HoodieCompactionPlan compactionPlan =
compactionPlanCache.computeIfAbsent(instant, k -> {
- try {
- return CompactionUtils.getCompactionPlan(
- this.writeClient.getHoodieTable().getMetaClient(), instant);
- } catch (Exception e) {
- throw new HoodieException(e);
- }
- });
-
- boolean isReady = compactionPlan.getOperations().size() == events.size();
- if (!isReady) {
- return;
- }
-
- if (events.stream().anyMatch(CompactionCommitEvent::isFailed)) {
- try {
- // handle failure case
- CompactionUtil.rollbackCompaction(table, instant,
writeClient.getTransactionManager());
- } finally {
- // remove commitBuffer to avoid obsolete metadata commit
- reset(instant);
- this.compactionMetrics.markCompactionRolledBack();
- }
- return;
- }
-
- try {
- doCommit(instant, events);
- } catch (Throwable throwable) {
- // make it fail-safe
- log.error("Error while committing compaction instant: {}", instant,
throwable);
- this.compactionMetrics.markCompactionRolledBack();
- } finally {
- // reset the status
- reset(instant);
- }
- }
-
- @SuppressWarnings("unchecked")
- private void doCommit(String instant, Collection<CompactionCommitEvent>
events) throws IOException {
- List<WriteStatus> statuses = events.stream()
- .map(CompactionCommitEvent::getWriteStatuses)
- .flatMap(Collection::stream)
- .collect(Collectors.toList());
-
- long numErrorRecords =
statuses.stream().map(WriteStatus::getTotalErrorRecords).reduce(Long::sum).orElse(0L);
-
- if (numErrorRecords > 0 && !this.conf.get(FlinkOptions.IGNORE_FAILED)) {
- // handle failure case
- log.error("Got {} error records during compaction of instant {},\n"
- + "option '{}' is configured as false,"
- + "rolls back the compaction", numErrorRecords, instant,
FlinkOptions.IGNORE_FAILED.key());
- CompactionUtil.rollbackCompaction(table, instant,
writeClient.getTransactionManager());
- this.compactionMetrics.markCompactionRolledBack();
- return;
- }
-
- HoodieCommitMetadata metadata =
CompactHelpers.getInstance().createCompactionMetadata(
- table, instant, HoodieListData.eager(statuses),
writeClient.getConfig().getSchema());
-
- // commit the compaction
- this.writeClient.completeCompaction(metadata, table, instant);
-
- this.compactionMetrics.updateCommitMetrics(instant, metadata);
- this.compactionMetrics.markCompactionCompleted();
-
- // Whether to clean up the old log file when compaction
- if (!conf.get(FlinkOptions.CLEAN_ASYNC_ENABLED) && !isCleaning) {
- this.writeClient.clean();
- }
- }
-
- private void reset(String instant) {
- this.commitBuffer.remove(instant);
- this.compactionPlanCache.remove(instant);
+ compactCommitHandler.commitIfNecessary(event);
}
- private void registerMetrics() {
- MetricGroup metrics = getRuntimeContext().getMetricGroup();
- compactionMetrics = new FlinkCompactionMetrics(metrics);
- compactionMetrics.registerMetrics();
+ @Override
+ public void close() throws Exception {
+ IOUtils.closeAll(compactCommitHandler, super::close);
}
}
diff --git
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/clustering/TestClusteringCommitSink.java
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/clustering/TestClusteringCommitSink.java
index e3486f3d3cc5..9e55ee0d9999 100644
---
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/clustering/TestClusteringCommitSink.java
+++
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/clustering/TestClusteringCommitSink.java
@@ -43,15 +43,19 @@ import org.apache.hudi.util.FlinkWriteClients;
import org.apache.flink.configuration.Configuration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.MockedStatic;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
@@ -68,6 +72,7 @@ class TestClusteringCommitSink {
private Configuration conf;
private HoodieFlinkWriteClient writeClient;
+ private HoodieFlinkWriteClient cleaningClient;
private HoodieFlinkTable table;
private HoodieTableMetaClient metaClient;
private HoodieActiveTimeline activeTimeline;
@@ -80,6 +85,7 @@ class TestClusteringCommitSink {
conf = new Configuration();
conf.set(FlinkOptions.CLEAN_ASYNC_ENABLED, false);
writeClient = mock(HoodieFlinkWriteClient.class);
+ cleaningClient = mock(HoodieFlinkWriteClient.class);
table = mock(HoodieFlinkTable.class);
metaClient = mock(HoodieTableMetaClient.class);
activeTimeline = mock(HoodieActiveTimeline.class);
@@ -183,8 +189,10 @@ class TestClusteringCommitSink {
}
}
- @Test
- void testCommitSuccessfulClusteringAndCleanInline() throws Exception {
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testCommitSuccessfulClusteringAndCloseClients(boolean asyncCleaning)
throws Exception {
+ conf.set(FlinkOptions.CLEAN_ASYNC_ENABLED, asyncCleaning);
ClusteringCommitSink sink = openSink();
HoodieClusteringPlan plan = planWithGroups(1);
HoodieWriteStat writeStat = new HoodieWriteStat();
@@ -213,7 +221,21 @@ class TestClusteringCommitSink {
any(HoodieCommitMetadata.class),
same(table),
eq(INSTANT));
- verify(writeClient).clean();
+ sink.close();
+ verify(writeClient, times(asyncCleaning ? 0 : 1)).clean();
+ verify(writeClient).close();
+ verify(cleaningClient, times(asyncCleaning ? 1 : 0)).clean();
+ verify(cleaningClient, times(asyncCleaning ? 1 : 0)).close();
+ }
+
+ @Test
+ void testClosesClusteringClientWhenCleaningClientCloseFails() throws
Exception {
+ conf.set(FlinkOptions.CLEAN_ASYNC_ENABLED, true);
+ doThrow(new RuntimeException("expected")).when(cleaningClient).close();
+ ClusteringCommitSink sink = openSink();
+ assertThrows(RuntimeException.class, sink::close);
+ verify(cleaningClient).close();
+ verify(writeClient).close();
}
private ClusteringCommitSink openSink() throws Exception {
@@ -221,9 +243,14 @@ class TestClusteringCommitSink {
MockStreamingRuntimeContext runtimeContext = new
MockStreamingRuntimeContext(false, 1, 0);
sink.setRuntimeContext(runtimeContext);
try (MockedStatic<FlinkWriteClients> writeClients =
mockStatic(FlinkWriteClients.class)) {
- writeClients.when(() -> FlinkWriteClients.createWriteClient(conf,
runtimeContext))
- .thenReturn(writeClient);
+ if (conf.get(FlinkOptions.CLEAN_ASYNC_ENABLED)) {
+ writeClients.when(() -> FlinkWriteClients.createWriteClient(conf,
runtimeContext)).thenReturn(cleaningClient, writeClient);
+ } else {
+ writeClients.when(() -> FlinkWriteClients.createWriteClient(conf,
runtimeContext)).thenReturn(writeClient);
+ }
sink.open(conf);
+ writeClients.verify(() -> FlinkWriteClients.createWriteClient(conf,
runtimeContext),
+ times(conf.get(FlinkOptions.CLEAN_ASYNC_ENABLED) ? 2 : 1));
}
return sink;
}
diff --git
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/compact/CompactionCommitTestSink.java
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/compact/CompactionCommitTestSink.java
index e9f8f72f593f..cb2057c1506d 100644
---
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/compact/CompactionCommitTestSink.java
+++
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/compact/CompactionCommitTestSink.java
@@ -18,12 +18,14 @@
package org.apache.hudi.sink.compact;
+import org.apache.hudi.client.HoodieFlinkWriteClient;
import org.apache.hudi.client.common.HoodieFlinkEngineContext;
import org.apache.hudi.client.timeline.HoodieTimelineArchiver;
import org.apache.hudi.client.timeline.TimelineArchiverV2;
import org.apache.hudi.common.table.timeline.HoodieInstant;
import org.apache.hudi.config.HoodieArchivalConfig;
import org.apache.hudi.exception.HoodieException;
+import org.apache.hudi.util.FlinkWriteClients;
import org.apache.hudi.utils.RuntimeContextUtils;
import org.apache.flink.configuration.Configuration;
@@ -34,23 +36,28 @@ import java.util.List;
* CompactionCommitTestSink, throw for first attempt to simulate failure
*/
public class CompactionCommitTestSink extends CompactionCommitSink {
+ private final Configuration conf;
+
public CompactionCommitTestSink(Configuration conf) {
super(conf);
+ this.conf = conf;
}
@Override
public void invoke(CompactionCommitEvent event, Context context) throws
Exception {
super.invoke(event, context);
- List<HoodieInstant> instants =
writeClient.getHoodieTable().getMetaClient().getActiveTimeline().getInstants();
- boolean compactCommitted = instants.stream().anyMatch(i ->
i.requestedTime().equals(event.getInstant()) && i.isCompleted());
- if (compactCommitted &&
RuntimeContextUtils.getAttemptNumber(getRuntimeContext()) == 0) {
- // archive compact instant
-
this.writeClient.getConfig().setValue(HoodieArchivalConfig.MAX_COMMITS_TO_KEEP,
"1");
-
this.writeClient.getConfig().setValue(HoodieArchivalConfig.MIN_COMMITS_TO_KEEP,
"1");
- HoodieTimelineArchiver archiver = new
TimelineArchiverV2(this.writeClient.getConfig(),
this.writeClient.getHoodieTable());
- this.writeClient.getHoodieTable().getMetaClient().reloadActiveTimeline();
- archiver.archiveIfRequired(HoodieFlinkEngineContext.DEFAULT);
- throw new HoodieException("Fail first attempt to simulate failover in
test.");
+ try (HoodieFlinkWriteClient writeClient =
FlinkWriteClients.createWriteClient(conf, getRuntimeContext())) {
+ List<HoodieInstant> instants =
writeClient.getHoodieTable().getMetaClient().getActiveTimeline().getInstants();
+ boolean compactCommitted = instants.stream().anyMatch(i ->
i.requestedTime().equals(event.getInstant()) && i.isCompleted());
+ if (compactCommitted &&
RuntimeContextUtils.getAttemptNumber(getRuntimeContext()) == 0) {
+ // archive compact instant
+
writeClient.getConfig().setValue(HoodieArchivalConfig.MAX_COMMITS_TO_KEEP, "1");
+
writeClient.getConfig().setValue(HoodieArchivalConfig.MIN_COMMITS_TO_KEEP, "1");
+ HoodieTimelineArchiver archiver = new
TimelineArchiverV2(writeClient.getConfig(), writeClient.getHoodieTable());
+ writeClient.getHoodieTable().getMetaClient().reloadActiveTimeline();
+ archiver.archiveIfRequired(HoodieFlinkEngineContext.DEFAULT);
+ throw new HoodieException("Fail first attempt to simulate failover in
test.");
+ }
}
}
}
diff --git
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/compact/TestCompactionCommitSink.java
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/compact/TestCompactionCommitSink.java
new file mode 100644
index 000000000000..2a2ecff94fef
--- /dev/null
+++
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/compact/TestCompactionCommitSink.java
@@ -0,0 +1,87 @@
+/*
+ * 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.compact;
+
+import org.apache.hudi.client.HoodieFlinkWriteClient;
+import org.apache.hudi.configuration.FlinkOptions;
+import org.apache.hudi.sink.compact.handler.CleanHandler;
+import org.apache.hudi.sink.compact.handler.CompactionCommitHandler;
+import org.apache.hudi.sink.compact.handler.TableServiceHandlerFactory;
+import org.apache.hudi.sink.utils.MockStreamingRuntimeContext;
+import org.apache.hudi.sink.v2.compact.CompactionCommitSinkV2;
+import org.apache.hudi.util.FlinkWriteClients;
+
+import org.apache.flink.configuration.Configuration;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.InOrder;
+import org.mockito.MockedStatic;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+
+/** Tests cleanup failure handling across both compaction sink APIs. */
+class TestCompactionCommitSink {
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testClosePreservesCommitFailureAndSuppressesCleaningFailure(boolean
useSinkV2) throws Exception {
+ Configuration conf = new Configuration();
+ conf.set(FlinkOptions.CLEAN_ASYNC_ENABLED, true);
+ MockStreamingRuntimeContext runtimeContext = new
MockStreamingRuntimeContext(false, 1, 0);
+ HoodieFlinkWriteClient writeClient = mock(HoodieFlinkWriteClient.class);
+ CompactionCommitHandler commitHandler =
mock(CompactionCommitHandler.class);
+ CleanHandler cleanHandler = mock(CleanHandler.class);
+ RuntimeException commitFailure = new RuntimeException("commit handler
close failed");
+ RuntimeException cleanFailure = new RuntimeException("clean handler close
failed");
+ doThrow(commitFailure).when(commitHandler).close();
+ doThrow(cleanFailure).when(cleanHandler).close();
+
+ try (MockedStatic<FlinkWriteClients> clients =
mockStatic(FlinkWriteClients.class);
+ MockedStatic<TableServiceHandlerFactory> factory =
mockStatic(TableServiceHandlerFactory.class)) {
+ clients.when(() -> FlinkWriteClients.createWriteClient(conf,
runtimeContext)).thenReturn(writeClient);
+ factory.when(() -> TableServiceHandlerFactory.createCleanHandler(conf,
writeClient)).thenReturn(cleanHandler);
+ factory.when(() ->
TableServiceHandlerFactory.createCompactionCommitHandler(conf,
runtimeContext)).thenReturn(commitHandler);
+
+ AutoCloseable sink;
+ if (useSinkV2) {
+ CompactionCommitSinkV2 function = new CompactionCommitSinkV2(conf);
+ function.setRuntimeContext(runtimeContext);
+ function.open(conf);
+ sink = function::close;
+ } else {
+ CompactionCommitSink function = new CompactionCommitSink(conf);
+ function.setRuntimeContext(runtimeContext);
+ function.open(conf);
+ sink = function::close;
+ }
+
+ assertSame(commitFailure, assertThrows(RuntimeException.class,
sink::close));
+ assertArrayEquals(new Throwable[] {cleanFailure},
commitFailure.getSuppressed());
+ InOrder order = inOrder(commitHandler, cleanHandler);
+ order.verify(commitHandler).close();
+ order.verify(cleanHandler).close();
+ }
+ }
+}
diff --git
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/compact/handler/TestDefaultCleanHandler.java
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/compact/handler/TestDefaultCleanHandler.java
index 7c2c5524d49d..56ff5260f3bb 100644
---
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/compact/handler/TestDefaultCleanHandler.java
+++
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/compact/handler/TestDefaultCleanHandler.java
@@ -21,13 +21,60 @@ package org.apache.hudi.sink.compact.handler;
import org.apache.hudi.client.HoodieFlinkWriteClient;
import org.junit.jupiter.api.Test;
+import org.mockito.InOrder;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
class TestDefaultCleanHandler {
+ @Test
+ void testCloseWaitsForInitialCleaning() {
+ HoodieFlinkWriteClient writeClient = mock(HoodieFlinkWriteClient.class);
+ try (DefaultCleanHandler handler = new DefaultCleanHandler(writeClient)) {
+ handler.clean();
+ }
+ InOrder order = inOrder(writeClient);
+ order.verify(writeClient).clean();
+ order.verify(writeClient).close();
+ verifyNoMoreInteractions(writeClient);
+ }
+
+ @Test
+ void testCheckpointCleaningDoesNotStartTwice() {
+ HoodieFlinkWriteClient writeClient = mock(HoodieFlinkWriteClient.class);
+ try (DefaultCleanHandler handler = new DefaultCleanHandler(writeClient)) {
+ handler.startAsyncCleaning();
+ handler.startAsyncCleaning();
+ handler.waitForCleaningFinish();
+ }
+ InOrder order = inOrder(writeClient);
+ order.verify(writeClient).startAsyncCleaning();
+ order.verify(writeClient).waitForCleaningFinish();
+ order.verify(writeClient).close();
+ verifyNoMoreInteractions(writeClient);
+ }
+
+ @Test
+ void testCleaningStartFailureAllowsRetry() {
+ HoodieFlinkWriteClient writeClient = mock(HoodieFlinkWriteClient.class);
+ doThrow(new
RuntimeException("expected")).doNothing().when(writeClient).startAsyncCleaning();
+ try (DefaultCleanHandler handler = new DefaultCleanHandler(writeClient)) {
+ assertDoesNotThrow(handler::startAsyncCleaning);
+ handler.startAsyncCleaning();
+ handler.waitForCleaningFinish();
+ }
+ verify(writeClient, times(2)).startAsyncCleaning();
+ verify(writeClient).waitForCleaningFinish();
+ verify(writeClient).close();
+ verifyNoMoreInteractions(writeClient);
+ }
+
@Test
void testCloseClosesWriteClientWithoutTriggeringClean() {
HoodieFlinkWriteClient writeClient = mock(HoodieFlinkWriteClient.class);
diff --git
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/compact/handler/TestTableServiceHandlerFactory.java
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/compact/handler/TestTableServiceHandlerFactory.java
new file mode 100644
index 000000000000..4d7a84236c81
--- /dev/null
+++
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/compact/handler/TestTableServiceHandlerFactory.java
@@ -0,0 +1,172 @@
+/*
+ * 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.compact.handler;
+
+import org.apache.hudi.client.HoodieFlinkWriteClient;
+import org.apache.hudi.configuration.FlinkOptions;
+import org.apache.hudi.index.HoodieIndex;
+import org.apache.hudi.table.HoodieFlinkTable;
+import org.apache.hudi.util.FlinkWriteClients;
+import org.apache.hudi.util.StreamerUtil;
+
+import org.apache.flink.api.common.functions.RuntimeContext;
+import org.apache.flink.configuration.Configuration;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.MockedStatic;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+/** Tests write-client ownership in the compaction commit handler factory. */
+class TestTableServiceHandlerFactory {
+ private Configuration conf;
+ private RuntimeContext runtimeContext;
+ private HoodieFlinkWriteClient dataClient;
+ private HoodieFlinkWriteClient metadataClient;
+
+ @BeforeEach
+ @SuppressWarnings("unchecked")
+ void setUp() {
+ conf = new Configuration();
+ runtimeContext = mock(RuntimeContext.class);
+ dataClient = mock(HoodieFlinkWriteClient.class);
+ metadataClient = mock(HoodieFlinkWriteClient.class);
+ when(dataClient.getHoodieTable()).thenReturn(mock(HoodieFlinkTable.class));
+
when(metadataClient.getHoodieTable()).thenReturn(mock(HoodieFlinkTable.class));
+ }
+
+ @ParameterizedTest
+ @CsvSource({"false,false", "true,false", "false,true", "true,true"})
+ void testReturnedHandlerOwnsCreatedClients(boolean dataCompaction, boolean
metadataCompaction) {
+ configureCompaction(dataCompaction, metadataCompaction);
+ try (CompactionCommitHandler handler = createHandler()) {
+ if (dataCompaction && metadataCompaction) {
+ assertInstanceOf(CompositeCompactionCommitHandler.class, handler);
+ } else if (metadataCompaction) {
+ assertInstanceOf(MetadataTableCompactionCommitHandler.class, handler);
+ } else {
+ assertInstanceOf(DataTableCompactionCommitHandler.class, handler);
+ }
+ verify(dataClient, never()).close();
+ verify(metadataClient, never()).close();
+ }
+ verify(dataClient).close();
+ if (metadataCompaction) {
+ verify(metadataClient).close();
+ } else {
+ verifyNoInteractions(metadataClient);
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testMetadataClientCreationFailureClosesDataClient(boolean
dataCompaction) {
+ configureCompaction(dataCompaction, true);
+ RuntimeException failure = new RuntimeException("metadata client creation
failed");
+ RuntimeException closeFailure = new RuntimeException("data client close
failed");
+ doThrow(closeFailure).when(dataClient).close();
+ try (MockedStatic<FlinkWriteClients> writeClients =
mockStatic(FlinkWriteClients.class);
+ MockedStatic<StreamerUtil> streamerUtil =
mockStatic(StreamerUtil.class)) {
+ writeClients.when(() -> FlinkWriteClients.createWriteClient(conf,
runtimeContext)).thenReturn(dataClient);
+ streamerUtil.when(() ->
StreamerUtil.createMetadataWriteClient(dataClient)).thenThrow(failure);
+
+ assertSame(failure, assertThrows(RuntimeException.class,
+ () -> TableServiceHandlerFactory.createCompactionCommitHandler(conf,
runtimeContext)));
+ }
+ verify(dataClient).close();
+ verifyNoInteractions(metadataClient);
+ assertArrayEquals(new Throwable[] {closeFailure}, failure.getSuppressed());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testMetadataHandlerConstructionFailureClosesBothClients(boolean
dataCompaction) {
+ configureCompaction(dataCompaction, true);
+ RuntimeException failure = new RuntimeException("metadata handler
construction failed");
+ RuntimeException metadataCloseFailure = new RuntimeException("metadata
client close failed");
+ RuntimeException dataCloseFailure = new RuntimeException("data client
close failed");
+ when(metadataClient.getHoodieTable()).thenThrow(failure);
+ doThrow(metadataCloseFailure).when(metadataClient).close();
+ doThrow(dataCloseFailure).when(dataClient).close();
+
+ assertSame(failure, assertThrows(RuntimeException.class,
this::createHandler));
+
+ verify(metadataClient).close();
+ verify(dataClient).close();
+ assertArrayEquals(new Throwable[] {metadataCloseFailure,
dataCloseFailure}, failure.getSuppressed());
+ }
+
+ @Test
+ void testDataHandlerConstructionFailureClosesDataClient() {
+ configureCompaction(true, false);
+ RuntimeException failure = new RuntimeException("data handler construction
failed");
+ when(dataClient.getHoodieTable()).thenThrow(failure);
+
+ assertSame(failure, assertThrows(RuntimeException.class,
this::createHandler));
+
+ verify(dataClient).close();
+ verifyNoInteractions(metadataClient);
+ }
+
+ @Test
+ void testMetadataOnlyCloseFailureStillClosesDataClient() {
+ configureCompaction(false, true);
+ CompactionCommitHandler handler = createHandler();
+ RuntimeException failure = new RuntimeException("metadata client close
failed");
+ RuntimeException closeFailure = new RuntimeException("data client close
failed");
+ doThrow(failure).when(metadataClient).close();
+ doThrow(closeFailure).when(dataClient).close();
+
+ assertSame(failure, assertThrows(RuntimeException.class, handler::close));
+
+ verify(metadataClient).close();
+ verify(dataClient).close();
+ assertArrayEquals(new Throwable[] {closeFailure}, failure.getSuppressed());
+ }
+
+ private CompactionCommitHandler createHandler() {
+ try (MockedStatic<FlinkWriteClients> writeClients =
mockStatic(FlinkWriteClients.class);
+ MockedStatic<StreamerUtil> streamerUtil =
mockStatic(StreamerUtil.class)) {
+ writeClients.when(() -> FlinkWriteClients.createWriteClient(conf,
runtimeContext)).thenReturn(dataClient);
+ streamerUtil.when(() ->
StreamerUtil.createMetadataWriteClient(dataClient)).thenReturn(metadataClient);
+ return TableServiceHandlerFactory.createCompactionCommitHandler(conf,
runtimeContext);
+ }
+ }
+
+ private void configureCompaction(boolean dataCompaction, boolean
metadataCompaction) {
+ conf.set(FlinkOptions.TABLE_TYPE, FlinkOptions.TABLE_TYPE_MERGE_ON_READ);
+ conf.set(FlinkOptions.COMPACTION_ASYNC_ENABLED, dataCompaction);
+ conf.set(FlinkOptions.METADATA_ENABLED, metadataCompaction);
+ conf.set(FlinkOptions.INDEX_TYPE,
HoodieIndex.IndexType.GLOBAL_RECORD_LEVEL_INDEX.name());
+ conf.set(FlinkOptions.METADATA_COMPACTION_ASYNC_ENABLED,
metadataCompaction);
+ }
+}
diff --git
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/v2/TestCleanFunctionV2.java
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/v2/TestCleanFunctionV2.java
index ca2d75cd3ff0..164aede00e9b 100644
---
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/v2/TestCleanFunctionV2.java
+++
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/v2/TestCleanFunctionV2.java
@@ -20,7 +20,12 @@ package org.apache.hudi.sink.v2;
import org.apache.hudi.client.HoodieFlinkWriteClient;
import org.apache.hudi.configuration.FlinkOptions;
+import org.apache.hudi.index.HoodieIndex;
+import org.apache.hudi.sink.compact.handler.CleanHandler;
+import org.apache.hudi.sink.compact.handler.DefaultCleanHandler;
+import org.apache.hudi.sink.compact.handler.TableServiceHandlerFactory;
import org.apache.hudi.util.FlinkWriteClients;
+import org.apache.hudi.util.StreamerUtil;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.operators.ProcessOperator;
@@ -28,21 +33,23 @@ import
org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
import org.apache.flink.table.data.RowData;
import org.junit.jupiter.api.Test;
+import org.mockito.InOrder;
+import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
-import java.util.concurrent.Callable;
-import java.util.concurrent.TimeUnit;
-
-import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
/** Tests checkpoint-driven cleaning in {@link CleanFunctionV2}. */
class TestCleanFunctionV2 {
@@ -52,46 +59,54 @@ class TestCleanFunctionV2 {
Configuration conf = new Configuration();
conf.set(FlinkOptions.CLEAN_ASYNC_ENABLED, true);
HoodieFlinkWriteClient writeClient = mock(HoodieFlinkWriteClient.class);
- CleanFunctionV2<String> function = new CleanFunctionV2<>(conf);
-
- try (OneInputStreamOperatorTestHarness<String, RowData> harness =
- openHarness(conf, function, writeClient)) {
- // Opening waits for any cleaning left by a previous job attempt.
- verify(writeClient, timeout(5000)).clean();
- assertTrue(waitUntil(() -> !function.isCleaning));
-
- harness.processElement(new StreamRecord<>("ignored"));
- assertTrue(harness.getOutput().isEmpty());
-
- harness.snapshot(1, 1);
- verify(writeClient).startAsyncCleaning();
- assertTrue(function.isCleaning);
-
- harness.notifyOfCompletedCheckpoint(1);
- verify(writeClient, timeout(5000)).waitForCleaningFinish();
- assertTrue(waitUntil(() -> !function.isCleaning));
+ CleanHandler cleanHandler = mock(CleanHandler.class);
+
+ try (MockedStatic<TableServiceHandlerFactory> factory =
mockStatic(TableServiceHandlerFactory.class)) {
+ factory.when(() -> TableServiceHandlerFactory.createCleanHandler(conf,
writeClient)).thenReturn(cleanHandler);
+ try (OneInputStreamOperatorTestHarness<String, RowData> harness =
openHarness(conf, writeClient)) {
+ harness.processElement(new StreamRecord<>("ignored"));
+ assertTrue(harness.getOutput().isEmpty());
+ harness.snapshot(1, 1);
+ harness.notifyOfCompletedCheckpoint(1);
+ }
}
- verify(writeClient).close();
+ InOrder order = inOrder(cleanHandler);
+ order.verify(cleanHandler).clean();
+ order.verify(cleanHandler).startAsyncCleaning();
+ order.verify(cleanHandler).waitForCleaningFinish();
+ order.verify(cleanHandler).close();
+ verifyNoMoreInteractions(cleanHandler);
+ // The handler owns the write client and closes it itself.
+ verify(writeClient, never()).close();
}
@Test
- void testSnapshotDoesNotPropagateCleaningFailure() throws Exception {
+ void testCleaningIncludesMetadataTable() throws Exception {
Configuration conf = new Configuration();
conf.set(FlinkOptions.CLEAN_ASYNC_ENABLED, true);
+ conf.set(FlinkOptions.METADATA_ENABLED, true);
+ conf.set(FlinkOptions.INDEX_TYPE,
HoodieIndex.IndexType.GLOBAL_RECORD_LEVEL_INDEX.name());
HoodieFlinkWriteClient writeClient = mock(HoodieFlinkWriteClient.class);
- CleanFunctionV2<String> function = new CleanFunctionV2<>(conf);
- doThrow(new
RuntimeException("expected")).when(writeClient).startAsyncCleaning();
-
- try (OneInputStreamOperatorTestHarness<String, RowData> harness =
- openHarness(conf, function, writeClient)) {
- verify(writeClient, timeout(5000)).clean();
- assertTrue(waitUntil(() -> !function.isCleaning));
-
- harness.snapshot(1, 1);
+ HoodieFlinkWriteClient metadataClient = mock(HoodieFlinkWriteClient.class);
+
+ try (MockedStatic<StreamerUtil> streamerUtil =
mockStatic(StreamerUtil.class);
+ MockedConstruction<DefaultCleanHandler> handlers =
mockConstruction(DefaultCleanHandler.class)) {
+ streamerUtil.when(() ->
StreamerUtil.createMetadataWriteClient(writeClient)).thenReturn(metadataClient);
+ try (OneInputStreamOperatorTestHarness<String, RowData> harness =
openHarness(conf, writeClient)) {
+ harness.snapshot(1, 1);
+ harness.notifyOfCompletedCheckpoint(1);
+ }
- assertFalse(function.isCleaning);
- verify(writeClient).startAsyncCleaning();
+ assertEquals(2, handlers.constructed().size());
+ for (CleanHandler handler : handlers.constructed()) {
+ InOrder order = inOrder(handler);
+ order.verify(handler).clean();
+ order.verify(handler).startAsyncCleaning();
+ order.verify(handler).waitForCleaningFinish();
+ order.verify(handler).close();
+ verifyNoMoreInteractions(handler);
+ }
}
}
@@ -100,43 +115,31 @@ class TestCleanFunctionV2 {
Configuration conf = new Configuration();
conf.set(FlinkOptions.CLEAN_ASYNC_ENABLED, false);
HoodieFlinkWriteClient writeClient = mock(HoodieFlinkWriteClient.class);
- CleanFunctionV2<String> function = new CleanFunctionV2<>(conf);
- try (OneInputStreamOperatorTestHarness<String, RowData> harness =
- openHarness(conf, function, writeClient)) {
- harness.snapshot(1, 1);
- harness.notifyOfCompletedCheckpoint(1);
- harness.processElement(new StreamRecord<>("ignored"));
-
- verify(writeClient, never()).clean();
- verify(writeClient, never()).startAsyncCleaning();
- verify(writeClient, never()).waitForCleaningFinish();
- assertTrue(harness.getOutput().isEmpty());
+ try (MockedStatic<TableServiceHandlerFactory> factory =
mockStatic(TableServiceHandlerFactory.class)) {
+ try (OneInputStreamOperatorTestHarness<String, RowData> harness =
openHarness(conf, writeClient)) {
+ harness.snapshot(1, 1);
+ harness.notifyOfCompletedCheckpoint(1);
+ harness.processElement(new StreamRecord<>("ignored"));
+ assertTrue(harness.getOutput().isEmpty());
+ }
+ factory.verifyNoInteractions();
}
+ verifyNoInteractions(writeClient);
}
private OneInputStreamOperatorTestHarness<String, RowData> openHarness(
Configuration conf,
- CleanFunctionV2<String> function,
HoodieFlinkWriteClient writeClient) throws Exception {
OneInputStreamOperatorTestHarness<String, RowData> harness =
- new OneInputStreamOperatorTestHarness<>(new
ProcessOperator<>(function), 1, 1, 0);
+ new OneInputStreamOperatorTestHarness<>(new ProcessOperator<>(new
CleanFunctionV2<String>(conf)), 1, 1, 0);
try (MockedStatic<FlinkWriteClients> writeClients =
mockStatic(FlinkWriteClients.class)) {
writeClients.when(() -> FlinkWriteClients.createWriteClient(
eq(conf), any())).thenReturn(writeClient);
harness.open();
+ writeClients.verify(() -> FlinkWriteClients.createWriteClient(eq(conf),
any()),
+ times(conf.get(FlinkOptions.CLEAN_ASYNC_ENABLED) ? 1 : 0));
}
return harness;
}
-
- private boolean waitUntil(Callable<Boolean> condition) throws Exception {
- long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
- while (System.nanoTime() < deadline) {
- if (condition.call()) {
- return true;
- }
- Thread.sleep(10);
- }
- return condition.call();
- }
}
diff --git
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/v2/clustering/TestClusteringCommitSinkV2.java
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/v2/clustering/TestClusteringCommitSinkV2.java
index 1614dfdd172f..5555891afde5 100644
---
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/v2/clustering/TestClusteringCommitSinkV2.java
+++
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/v2/clustering/TestClusteringCommitSinkV2.java
@@ -47,15 +47,19 @@ import
org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
import org.apache.flink.table.data.RowData;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.MockedStatic;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
@@ -70,6 +74,7 @@ class TestClusteringCommitSinkV2 {
private Configuration conf;
private HoodieFlinkWriteClient writeClient;
+ private HoodieFlinkWriteClient cleaningClient;
private HoodieFlinkTable table;
private HoodieTableMetaClient metaClient;
private HoodieActiveTimeline activeTimeline;
@@ -82,6 +87,7 @@ class TestClusteringCommitSinkV2 {
conf = new Configuration();
conf.set(FlinkOptions.CLEAN_ASYNC_ENABLED, false);
writeClient = mock(HoodieFlinkWriteClient.class);
+ cleaningClient = mock(HoodieFlinkWriteClient.class);
table = mock(HoodieFlinkTable.class);
metaClient = mock(HoodieTableMetaClient.class);
activeTimeline = mock(HoodieActiveTimeline.class);
@@ -170,8 +176,10 @@ class TestClusteringCommitSinkV2 {
}
}
- @Test
- void testCommitSuccessfulClusteringAndCleanInline() throws Exception {
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testCommitSuccessfulClusteringAndCloseClientOnce(boolean asyncCleaning)
throws Exception {
+ conf.set(FlinkOptions.CLEAN_ASYNC_ENABLED, asyncCleaning);
HoodieClusteringPlan plan = planWithGroups(1);
WriteStatus status = writeStatus("partition", "new-file", 0);
stubWriteConfig();
@@ -190,7 +198,20 @@ class TestClusteringCommitSinkV2 {
verify(metaClient).reloadActiveTimeline();
verify(writeClient).completeTableService(
eq(TableServiceType.CLUSTER), any(HoodieCommitMetadata.class),
same(table), eq(INSTANT));
- verify(writeClient).clean();
+ verify(writeClient, times(asyncCleaning ? 0 : 1)).clean();
+ verify(writeClient).close();
+ verify(cleaningClient, times(asyncCleaning ? 1 : 0)).clean();
+ verify(cleaningClient, times(asyncCleaning ? 1 : 0)).close();
+ }
+
+ @Test
+ void testClosesClusteringClientWhenCleaningClientCloseFails() throws
Exception {
+ conf.set(FlinkOptions.CLEAN_ASYNC_ENABLED, true);
+ doThrow(new RuntimeException("expected")).when(cleaningClient).close();
+ OneInputStreamOperatorTestHarness<ClusteringCommitEvent, RowData> harness
= openHarness();
+ assertThrows(Exception.class, harness::close);
+ verify(cleaningClient).close();
+ verify(writeClient).close();
}
private OneInputStreamOperatorTestHarness<ClusteringCommitEvent, RowData>
openHarness() throws Exception {
@@ -199,9 +220,14 @@ class TestClusteringCommitSinkV2 {
OneInputStreamOperatorTestHarness<ClusteringCommitEvent, RowData> harness =
new OneInputStreamOperatorTestHarness<>(operator, 1, 1, 0);
try (MockedStatic<FlinkWriteClients> writeClients =
mockStatic(FlinkWriteClients.class)) {
- writeClients.when(() -> FlinkWriteClients.createWriteClient(
- eq(conf), any())).thenReturn(writeClient);
+ if (conf.get(FlinkOptions.CLEAN_ASYNC_ENABLED)) {
+ writeClients.when(() -> FlinkWriteClients.createWriteClient(eq(conf),
any())).thenReturn(cleaningClient, writeClient);
+ } else {
+ writeClients.when(() -> FlinkWriteClients.createWriteClient(eq(conf),
any())).thenReturn(writeClient);
+ }
harness.open();
+ writeClients.verify(() -> FlinkWriteClients.createWriteClient(eq(conf),
any()),
+ times(conf.get(FlinkOptions.CLEAN_ASYNC_ENABLED) ? 2 : 1));
}
return harness;
}
diff --git
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/v2/compact/TestCompactionCommitSinkV2.java
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/v2/compact/TestCompactionCommitSinkV2.java
index 925e5bb2d479..e08e7ab0b23c 100644
---
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/v2/compact/TestCompactionCommitSinkV2.java
+++
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/v2/compact/TestCompactionCommitSinkV2.java
@@ -25,15 +25,21 @@ import org.apache.hudi.client.WriteStatus;
import org.apache.hudi.client.transaction.TransactionManager;
import org.apache.hudi.common.model.HoodieCommitMetadata;
import org.apache.hudi.common.model.HoodieWriteStat;
+import org.apache.hudi.common.model.WriteOperationType;
import org.apache.hudi.common.table.HoodieTableMetaClient;
import org.apache.hudi.common.util.CompactionUtils;
import org.apache.hudi.config.HoodieWriteConfig;
import org.apache.hudi.configuration.FlinkOptions;
+import org.apache.hudi.index.HoodieIndex;
import org.apache.hudi.sink.compact.CompactionCommitEvent;
+import org.apache.hudi.sink.compact.handler.CleanHandler;
+import org.apache.hudi.sink.compact.handler.CompactionCommitHandler;
+import org.apache.hudi.sink.compact.handler.TableServiceHandlerFactory;
import org.apache.hudi.table.HoodieFlinkTable;
import org.apache.hudi.table.action.compact.CompactHelpers;
import org.apache.hudi.util.CompactionUtil;
import org.apache.hudi.util.FlinkWriteClients;
+import org.apache.hudi.util.StreamerUtil;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.operators.ProcessOperator;
@@ -42,19 +48,24 @@ import
org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
import org.apache.flink.table.data.RowData;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.MockedStatic;
import java.util.Arrays;
import java.util.List;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
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;
/** Tests {@link CompactionCommitSinkV2} with Flink's operator test harness. */
@@ -64,6 +75,7 @@ class TestCompactionCommitSinkV2 {
private Configuration conf;
private HoodieFlinkWriteClient writeClient;
+ private HoodieFlinkWriteClient cleaningClient;
private HoodieFlinkTable table;
private HoodieTableMetaClient metaClient;
private TransactionManager transactionManager;
@@ -74,6 +86,7 @@ class TestCompactionCommitSinkV2 {
conf = new Configuration();
conf.set(FlinkOptions.CLEAN_ASYNC_ENABLED, false);
writeClient = mock(HoodieFlinkWriteClient.class);
+ cleaningClient = mock(HoodieFlinkWriteClient.class);
table = mock(HoodieFlinkTable.class);
metaClient = mock(HoodieTableMetaClient.class);
transactionManager = mock(TransactionManager.class);
@@ -153,9 +166,11 @@ class TestCompactionCommitSinkV2 {
}
}
- @Test
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
@SuppressWarnings("unchecked")
- void testCommitSuccessfulCompactionAndCleanInline() throws Exception {
+ void testCommitSuccessfulCompactionAndClean(boolean asyncCleaning) throws
Exception {
+ conf.set(FlinkOptions.CLEAN_ASYNC_ENABLED, asyncCleaning);
WriteStatus status = writeStatus("partition", "file-1", 0);
HoodieCommitMetadata metadata = new HoodieCommitMetadata();
CompactHelpers compactHelpers = mock(CompactHelpers.class);
@@ -175,7 +190,82 @@ class TestCompactionCommitSinkV2 {
}
verify(writeClient).completeCompaction(same(metadata), same(table),
eq(INSTANT));
- verify(writeClient).clean();
+ verify(writeClient, times(asyncCleaning ? 0 : 1)).clean();
+ verify(writeClient).close();
+ verify(cleaningClient, times(asyncCleaning ? 1 : 0)).close();
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ @SuppressWarnings("unchecked")
+ void testMetadataCompactionUsesSeparateHandler(boolean logCompaction) throws
Exception {
+ conf.set(FlinkOptions.TABLE_TYPE, FlinkOptions.TABLE_TYPE_MERGE_ON_READ);
+ conf.set(FlinkOptions.COMPACTION_ASYNC_ENABLED, true);
+ conf.set(FlinkOptions.METADATA_ENABLED, true);
+ conf.set(FlinkOptions.INDEX_TYPE,
HoodieIndex.IndexType.GLOBAL_RECORD_LEVEL_INDEX.name());
+ conf.set(FlinkOptions.METADATA_COMPACTION_ASYNC_ENABLED, true);
+ HoodieFlinkWriteClient metadataClient = mock(HoodieFlinkWriteClient.class);
+ HoodieFlinkTable metadataTable = mock(HoodieFlinkTable.class);
+ HoodieTableMetaClient metadataMetaClient =
mock(HoodieTableMetaClient.class);
+ when(metadataClient.getHoodieTable()).thenReturn(metadataTable);
+ when(metadataTable.getMetaClient()).thenReturn(metadataMetaClient);
+ stubWriteConfig();
+ HoodieWriteConfig writeConfig = writeClient.getConfig();
+ when(metadataClient.getConfig()).thenReturn(writeConfig);
+ CompactHelpers compactHelpers = mock(CompactHelpers.class);
+ HoodieCommitMetadata metadata = new HoodieCommitMetadata();
+ when(compactHelpers.createCompactionMetadata(same(metadataTable),
eq(INSTANT), any(), eq("{}"),
+ eq(logCompaction ? WriteOperationType.LOG_COMPACT :
WriteOperationType.COMPACT))).thenReturn(metadata);
+
+ HoodieCompactionPlan dataPlan = planWithOperations(2);
+ HoodieCompactionPlan metadataPlan = planWithOperations(1);
+ try (MockedStatic<StreamerUtil> streamerUtil =
mockStatic(StreamerUtil.class);
+ MockedStatic<CompactionUtils> compactionUtils =
mockStatic(CompactionUtils.class);
+ MockedStatic<CompactHelpers> helpersFactory =
mockStatic(CompactHelpers.class)) {
+ streamerUtil.when(() ->
StreamerUtil.createMetadataWriteClient(writeClient)).thenReturn(metadataClient);
+
helpersFactory.when(CompactHelpers::getInstance).thenReturn(compactHelpers);
+ compactionUtils.when(() -> CompactionUtils.getCompactionPlan(metaClient,
INSTANT)).thenReturn(dataPlan);
+ if (logCompaction) {
+ compactionUtils.when(() ->
CompactionUtils.getLogCompactionPlan(metadataMetaClient,
INSTANT)).thenReturn(metadataPlan);
+ } else {
+ compactionUtils.when(() ->
CompactionUtils.getCompactionPlan(metadataMetaClient,
INSTANT)).thenReturn(metadataPlan);
+ }
+ try (OneInputStreamOperatorTestHarness<CompactionCommitEvent, RowData>
harness = openHarness()) {
+ // Identical instant and file IDs must not mix the data and metadata
buffers.
+ process(harness, successEvent("file-1", new WriteStatus()));
+ process(harness, new CompactionCommitEvent(INSTANT, "file-1",
+ Arrays.asList(writeStatus("partition", "file-1", 0)), 0, true,
logCompaction));
+ }
+ }
+
+ if (logCompaction) {
+ verify(metadataClient).completeLogCompaction(same(metadata),
same(metadataTable), eq(INSTANT));
+ verify(metadataClient, never()).completeCompaction(any(), any(), any());
+ } else {
+ verify(metadataClient).completeCompaction(same(metadata),
same(metadataTable), eq(INSTANT));
+ verify(metadataClient, never()).completeLogCompaction(any(), any(),
any());
+ }
+ verify(writeClient, never()).completeCompaction(any(), any(), any());
+ verify(metadataClient).close();
+ verify(writeClient).close();
+ verifyNoInteractions(cleaningClient);
+ }
+
+ @Test
+ void testClosesCleanHandlerWhenCommitHandlerCloseFails() throws Exception {
+ conf.set(FlinkOptions.CLEAN_ASYNC_ENABLED, true);
+ CleanHandler cleanHandler = mock(CleanHandler.class);
+ CompactionCommitHandler handler = mock(CompactionCommitHandler.class);
+ doThrow(new RuntimeException("expected")).when(handler).close();
+ try (MockedStatic<TableServiceHandlerFactory> factory =
mockStatic(TableServiceHandlerFactory.class)) {
+ factory.when(() ->
TableServiceHandlerFactory.createCompactionCommitHandler(eq(conf),
any())).thenReturn(handler);
+ factory.when(() -> TableServiceHandlerFactory.createCleanHandler(conf,
cleaningClient)).thenReturn(cleanHandler);
+ OneInputStreamOperatorTestHarness<CompactionCommitEvent, RowData>
harness = openHarness();
+ assertThrows(Exception.class, harness::close);
+ }
+ verify(handler).registerMetrics(any());
+ verify(handler).close();
+ verify(cleanHandler).close();
}
private OneInputStreamOperatorTestHarness<CompactionCommitEvent, RowData>
openHarness() throws Exception {
@@ -184,9 +274,15 @@ class TestCompactionCommitSinkV2 {
OneInputStreamOperatorTestHarness<CompactionCommitEvent, RowData> harness =
new OneInputStreamOperatorTestHarness<>(operator, 1, 1, 0);
try (MockedStatic<FlinkWriteClients> writeClients =
mockStatic(FlinkWriteClients.class)) {
- writeClients.when(() -> FlinkWriteClients.createWriteClient(
- eq(conf), any())).thenReturn(writeClient);
+ if (conf.get(FlinkOptions.CLEAN_ASYNC_ENABLED)) {
+ writeClients.when(() -> FlinkWriteClients.createWriteClient(eq(conf),
any())).thenReturn(cleaningClient, writeClient);
+ } else {
+ writeClients.when(() -> FlinkWriteClients.createWriteClient(eq(conf),
any())).thenReturn(writeClient);
+ }
harness.open();
+ if (!conf.get(FlinkOptions.CLEAN_ASYNC_ENABLED)) {
+ writeClients.verify(() ->
FlinkWriteClients.createWriteClient(eq(conf), any()));
+ }
}
return harness;
}