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

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


The following commit(s) were added to refs/heads/master by this push:
     new acbe341c9c74 fix(test): cover SparkContext shutdown path in 
HoodieMetadataTableValidator (#19349)
acbe341c9c74 is described below

commit acbe341c9c741eff55bd779d219f3b14fa868335
Author: Lokesh Jain <[email protected]>
AuthorDate: Fri Aug 14 14:20:37 2026 +0530

    fix(test): cover SparkContext shutdown path in HoodieMetadataTableValidator 
(#19349)
---
 .../TestHoodieMetadataTableValidator.java          | 102 +++++++++++++++++++++
 1 file changed, 102 insertions(+)

diff --git 
a/hudi-utilities/src/test/java/org/apache/hudi/utilities/TestHoodieMetadataTableValidator.java
 
b/hudi-utilities/src/test/java/org/apache/hudi/utilities/TestHoodieMetadataTableValidator.java
index f3eaa254eeb5..af8e3b4d49c6 100644
--- 
a/hudi-utilities/src/test/java/org/apache/hudi/utilities/TestHoodieMetadataTableValidator.java
+++ 
b/hudi-utilities/src/test/java/org/apache/hudi/utilities/TestHoodieMetadataTableValidator.java
@@ -60,6 +60,7 @@ import org.apache.hudi.common.util.StringUtils;
 import org.apache.hudi.common.util.collection.Pair;
 import org.apache.hudi.config.HoodieCompactionConfig;
 import org.apache.hudi.config.HoodieWriteConfig;
+import org.apache.hudi.exception.HoodieException;
 import org.apache.hudi.exception.HoodieIOException;
 import org.apache.hudi.exception.HoodieValidationException;
 import org.apache.hudi.hadoop.fs.HadoopFSUtils;
@@ -81,6 +82,7 @@ import lombok.Setter;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.FileSystem;
 import org.apache.hadoop.fs.Path;
+import org.apache.spark.SparkException;
 import org.apache.spark.api.java.JavaPairRDD;
 import org.apache.spark.api.java.JavaRDD;
 import org.apache.spark.api.java.JavaSparkContext;
@@ -1677,6 +1679,106 @@ public class TestHoodieMetadataTableValidator extends 
HoodieSparkClientTestBase
         fsFileSlices.size(), mdtFileSlices.size())));
   }
 
+  @Test
+  void 
testDoMetadataTableValidationThrowsHoodieExceptionOnSparkContextShutdown() 
throws Exception {
+    Map<String, String> writeOptions = new HashMap<>();
+    writeOptions.put(DataSourceWriteOptions.TABLE_NAME().key(), "test_table");
+    writeOptions.put("hoodie.table.name", "test_table");
+    writeOptions.put(DataSourceWriteOptions.TABLE_TYPE().key(), 
"MERGE_ON_READ");
+    writeOptions.put(DataSourceWriteOptions.RECORDKEY_FIELD().key(), 
"_row_key");
+    writeOptions.put(DataSourceWriteOptions.PRECOMBINE_FIELD().key(), 
"timestamp");
+    writeOptions.put(DataSourceWriteOptions.PARTITIONPATH_FIELD().key(), 
"partition_path");
+
+    // Write with RLI enabled so checkMetadataTableIsAvailable() returns true 
and
+    // doMetadataTableValidation() proceeds to call validateRecordIndex.
+    // File-slice validation flags are intentionally NOT set so 
validateFilesInPartition
+    // is a no-op and cannot mask the SparkContext-shutdown exception we are 
testing.
+    makeInsertDf("000", 5).write().format("hudi").options(writeOptions)
+        .option(DataSourceWriteOptions.OPERATION().key(), 
WriteOperationType.BULK_INSERT.value())
+        
.option(HoodieMetadataConfig.GLOBAL_RECORD_LEVEL_INDEX_ENABLE_PROP.key(), 
"true")
+        
.option(HoodieMetadataConfig.GLOBAL_RECORD_LEVEL_INDEX_MIN_FILE_GROUP_COUNT_PROP.key(),
 "1")
+        
.option(HoodieMetadataConfig.GLOBAL_RECORD_LEVEL_INDEX_MAX_FILE_GROUP_COUNT_PROP.key(),
 "1")
+        .mode(SaveMode.Overwrite)
+        .save(basePath);
+
+    HoodieMetadataTableValidator.Config config = new 
HoodieMetadataTableValidator.Config();
+    config.basePath = "file:" + basePath;
+    // Do NOT enable validateLatestFileSlices / validateAllFileGroups: those 
call
+    // validateFilesInPartition inside a Spark map, and 
HoodieValidationException from
+    // that path would be re-thrown before validateRecordIndex is ever reached.
+
+    // NOTE: static nested class, not anonymous, so it does NOT capture
+    // TestHoodieMetadataTableValidator.this (which is not Serializable). An 
anonymous class
+    // would cause Spark's parallelize().map() to throw "Task not 
serializable", which the
+    // outer catch converts to HoodieValidationException instead of 
HoodieException.
+    HoodieMetadataTableValidator validator = new 
SparkContextShutdownValidator(jsc, config);
+    HoodieException ex = assertThrows(HoodieException.class, 
validator::doMetadataTableValidation);
+    assertFalse(ex instanceof HoodieValidationException,
+        "Expected HoodieException wrapping SparkContext shutdown, not 
HoodieValidationException.");
+  }
+
+  @Test
+  void 
testDoMetadataTableValidationThrowsHoodieValidationExceptionOnUnexpectedSparkFailure()
 throws Exception {
+    Map<String, String> writeOptions = new HashMap<>();
+    writeOptions.put(DataSourceWriteOptions.TABLE_NAME().key(), "test_table");
+    writeOptions.put("hoodie.table.name", "test_table");
+    writeOptions.put(DataSourceWriteOptions.TABLE_TYPE().key(), 
"MERGE_ON_READ");
+    writeOptions.put(DataSourceWriteOptions.RECORDKEY_FIELD().key(), 
"_row_key");
+    writeOptions.put(DataSourceWriteOptions.PRECOMBINE_FIELD().key(), 
"timestamp");
+    writeOptions.put(DataSourceWriteOptions.PARTITIONPATH_FIELD().key(), 
"partition_path");
+
+    makeInsertDf("000", 5).write().format("hudi").options(writeOptions)
+        .option(DataSourceWriteOptions.OPERATION().key(), 
WriteOperationType.BULK_INSERT.value())
+        
.option(HoodieMetadataConfig.GLOBAL_RECORD_LEVEL_INDEX_ENABLE_PROP.key(), 
"true")
+        
.option(HoodieMetadataConfig.GLOBAL_RECORD_LEVEL_INDEX_MIN_FILE_GROUP_COUNT_PROP.key(),
 "1")
+        
.option(HoodieMetadataConfig.GLOBAL_RECORD_LEVEL_INDEX_MAX_FILE_GROUP_COUNT_PROP.key(),
 "1")
+        .mode(SaveMode.Overwrite)
+        .save(basePath);
+
+    HoodieMetadataTableValidator.Config config = new 
HoodieMetadataTableValidator.Config();
+    config.basePath = "file:" + basePath;
+
+    // Covers the false branch of else if 
(ExceptionUtil.validateErrorMsg(...)):
+    // a SparkException with a non-cancellation message should still produce
+    // HoodieValidationException("Unexpected spark failure").
+    HoodieMetadataTableValidator validator = new 
UnexpectedSparkFailureValidator(jsc, config);
+    HoodieValidationException ex = 
assertThrows(HoodieValidationException.class, 
validator::doMetadataTableValidation);
+    assertTrue(ex.getMessage().contains("Unexpected spark failure"));
+  }
+
+  /** Static nested class; does NOT capture the enclosing test instance (not 
Serializable). */
+  private static final class SparkContextShutdownValidator extends 
HoodieMetadataTableValidator {
+    private static final long serialVersionUID = 1L;
+
+    SparkContextShutdownValidator(JavaSparkContext jsc, Config cfg) {
+      super(jsc, cfg);
+    }
+
+    @Override
+    void validateRecordIndex(HoodieSparkEngineContext sparkEngineContext, 
HoodieTableMetaClient metaClient) {
+      sneakyThrow(new SparkException("cancelled because SparkContext was shut 
down"));
+    }
+  }
+
+  /** Covers the false branch of the cancellation-message check. */
+  private static final class UnexpectedSparkFailureValidator extends 
HoodieMetadataTableValidator {
+    private static final long serialVersionUID = 1L;
+
+    UnexpectedSparkFailureValidator(JavaSparkContext jsc, Config cfg) {
+      super(jsc, cfg);
+    }
+
+    @Override
+    void validateRecordIndex(HoodieSparkEngineContext sparkEngineContext, 
HoodieTableMetaClient metaClient) {
+      sneakyThrow(new SparkException("some unexpected spark error"));
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private static <T extends Throwable> void sneakyThrow(Throwable e) throws T {
+    throw (T) e;
+  }
+
   private void mockPartitionWithFiles(List<String> partition1, HoodieStorage 
storage) throws IOException {
     for (String partition : partition1) {
       StoragePathInfo storagePathInfo = mock(StoragePathInfo.class);

Reply via email to