prakashgyank46-commits opened a new issue, #20325:
URL: https://github.com/apache/hudi/issues/20325

   ### Bug Description
   
   **What happened:**
   
   `SparkRDDWriteClient.scheduleLogCompaction(Option.empty())` fails with 
`NullPointerException: Expected a non-null value. Got null` in 
`HoodieCDCDataBlock` when a Merge-on-Read file slice contains CDC log blocks 
and the planner takes the block-metadata scanning path. Reproduced with the 
official Hudi 1.2.1 Spark 3.5 bundle, and separately with 1.1.0. No Hudi code 
was patched or scheduler checks bypassed.
   
   **What you expected:**
   
   Scheduling should inspect valid CDC log blocks without throwing. In this 
sparse workload no file group reaches the threshold, so it should return an 
empty instant, as the CDC-disabled control does, preserving snapshot/CDC 
semantics.
   
   **Steps to reproduce on 1.2.1:**
   
   1. Create a fresh MOR table, CDC enabled (`data_before_after`), partition 
`part`, record key `id`, ordering field `ts`. Disable automatic 
compaction/cleaning and metadata table.
   2. Bulk-insert rows `a`,`b` into partition `p`, then update both in three 
separate delta commits.
   3. Schedule, execute, and **commit regular compaction**. Verify a physical 
`.cdc` log file is generated.
   4. Update `a`,`b` in `p` in three more delta commits.
   5. Insert/update row `z` in partition `q` across seven separate delta 
commits. There are now **10 table-wide delta commits since regular 
compaction**, while `p` has only three subsequent data logs plus its CDC log.
   6. Set `hoodie.log.compaction.blocks.threshold=10`, construct the write 
client with the table schema, call `scheduleLogCompaction(Option.empty())`.
   7. The block metadata scanner throws the NPE below.
   
   **Trigger detail:** 1.2.1 has a table-wide delta-commit gate. Three 
post-compaction commits with threshold 10 return an early no-op and mask this 
failure. Ten commits distributed across file groups pass that gate while 
requiring scanning of a sparse file group. A lower threshold can bypass the 
scanner through its log-file-count fast path; that does not demonstrate a fix.
   
   **Validation:** 14 local scenarios: 3 successful log compactions, 7 clean 
no-ops, 4 expected scheduling NPEs. The four failures cover 
`data_before_after`, `data_before`, `op_key_only`, and CDC disabled in table 
properties after physical CDC logs were generated. The equivalent distributed 
CDC-disabled control returns a clean no-op. Thus existing CDC blocks are the 
trigger, not just the current CDC flag. Snapshot rows matched expected values 
before the attempt and were unchanged afterward. Full CDC query results 
remained unchanged where CDC stayed enabled. No data loss was observed; the 
demonstrated impact is failure to schedule log compaction.
   
   The complete standalone Java reproducer, configs, case results and source 
analysis are included below. Its `REPRODUCTION_VERIFIED cases=14 
expected_NPE=4` result means the bug was reproduced, not fixed.
   
   **Duplicate search:** No matching open or closed report found for 
`HoodieCDCDataBlock`, `HoodieLogBlockMetadataScanner`, `logcompaction 
NullPointerException`, or `log compaction CDC`.
   
   ### Environment
   
   **Hudi version:** 1.2.1, official 
`org.apache.hudi:hudi-spark3.5-bundle_2.12:1.2.1`, fresh table version 9. Also 
reproduced on 1.1.0; intermediate versions not runtime-tested.
   **Query engine:** Spark 3.5.6, Scala 2.12, `local[2]`.
   **Runtime:** OpenJDK 11.0.32.1, macOS 26.6/aarch64, local filesystem, 3 GB 
heap. No Docker, external catalog, Hive metastore or custom record merger.
   **Relevant configs:**
   ```properties
   hoodie.datasource.write.table.type=MERGE_ON_READ
   hoodie.datasource.write.recordkey.field=id
   hoodie.datasource.write.precombine.field=ts
   hoodie.datasource.write.partitionpath.field=part
   hoodie.table.cdc.enabled=true
   hoodie.table.cdc.supplemental.logging.mode=data_before_after
   hoodie.metadata.enable=false
   hoodie.compact.inline=false
   hoodie.compact.schedule.inline=false
   hoodie.compact.inline.max.delta.commits=1
   hoodie.clean.automatic=false
   hoodie.archive.automatic=false
   hoodie.log.compaction.enable=false
   hoodie.log.compaction.blocks.threshold=10
   hoodie.embed.timeline.server=false
   hoodie.write.markers.type=DIRECT
   ```
   Compaction/log compaction are explicitly invoked through the write client. 
Shuffle parallelism is 1; Spark uses Kryo and Hudi SQL extension/catalog. The 
full harness and options are included below.
   
   The unmodified Maven Central bundle was verified against published SHA-1 
`1120807b30891d6e32e4c71512b8f4fbfa2a8d50`.
   
   ### Logs and Stack Trace
   
   ```text
   java.lang.NullPointerException: Expected a non-null value. Got null
     at org.apache.hudi.common.util.Option.<init>(Option.java:65)
     at org.apache.hudi.common.util.Option.of(Option.java:76)
     at 
org.apache.hudi.common.table.log.block.HoodieCDCDataBlock.<init>(HoodieCDCDataBlock.java:45)
     at 
org.apache.hudi.common.table.log.HoodieLogFileReader.readBlock(HoodieLogFileReader.java:212)
     at 
org.apache.hudi.common.table.log.HoodieLogFileReader.next(HoodieLogFileReader.java:379)
     at 
org.apache.hudi.common.table.log.HoodieLogFormatReader.next(HoodieLogFormatReader.java:100)
     at 
org.apache.hudi.common.table.log.BaseHoodieLogRecordReader.scanInternal(BaseHoodieLogRecordReader.java:286)
     at 
org.apache.hudi.common.table.log.HoodieLogBlockMetadataScanner.<init>(HoodieLogBlockMetadataScanner.java:41)
     at 
org.apache.hudi.table.action.compact.plan.generators.HoodieLogCompactionPlanGenerator.isFileSliceEligibleForLogCompaction(HoodieLogCompactionPlanGenerator.java:104)
     at 
org.apache.hudi.table.action.compact.plan.generators.HoodieLogCompactionPlanGenerator.filterFileSlice(HoodieLogCompactionPlanGenerator.java:78)
     at 
org.apache.hudi.table.action.compact.plan.generators.BaseHoodieCompactionPlanGenerator.lambda$generateCompactionPlan$2(BaseHoodieCompactionPlanGenerator.java:132)
   ```
   
   **Root-cause analysis:** The metadata scanner creates a reader context 
without a schema handler. `BaseHoodieLogRecordReader` therefore has a null 
reader schema. `HoodieLogFileReader` passes that null into the CDC block 
constructor, which calls `Option.of(readerSchema)`.
   
   Relevant 1.2.1 source:
   
   - [Planner and file-count fast 
path](https://github.com/apache/hudi/blob/release-1.2.1/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/compact/plan/generators/HoodieLogCompactionPlanGenerator.java)
   - [Metadata 
scanner](https://github.com/apache/hudi/blob/release-1.2.1/hudi-common/src/main/java/org/apache/hudi/common/table/log/HoodieLogBlockMetadataScanner.java)
   - [CDC block 
constructor](https://github.com/apache/hudi/blob/release-1.2.1/hudi-common/src/main/java/org/apache/hudi/common/table/log/block/HoodieCDCDataBlock.java)
   
   A fix should cover both CDC-enabled tables and retained CDC logs after 
disabling CDC, and preserve CDC readability. A null-handling change alone 
should not be assumed sufficient without checking the scanner's handling of CDC 
block types.
   
   **Duplicate search:** Searched open and closed upstream issues for 
`HoodieCDCDataBlock`, `HoodieLogBlockMetadataScanner`, `logcompaction 
NullPointerException`, and `log compaction CDC`; no matching report was found.
   
   ### Reproducer
   
   The standalone harness below was compiled and run against the official 1.2.1 
bundle with the Spark 3.5.6 runtime. Its assertions verify the observed bug and 
control outcomes; `REPRODUCTION_VERIFIED cases=14 expected_NPE=4` means the 
reproduction succeeded, not that the bug is fixed. The Spark application name 
has been generalized for this report.
   
   Save the Java source as `CdcLogCompactionRepro.java`. With Java 11 and 
`REPRO_CP` containing the Spark 3.5.6 runtime dependencies plus the Hudi 1.2.1 
Spark 3.5/Scala 2.12 bundle (and no older Hudi bundle):
   
   ```bash
   mkdir -p evidence classes
   javac -cp "$REPRO_CP" -d classes CdcLogCompactionRepro.java
   SPARK_LOCAL_IP=127.0.0.1 java -Xmx3g -cp "classes:$REPRO_CP" 
CdcLogCompactionRepro
   ```
   
   The test creates fresh disposable tables beneath `evidence/` and retains 
them for inspection. It does not connect to a cluster or use any private 
dataset.
   
   
   <details>
   <summary>Complete Java reproducer (14 cases)</summary>
   
   ```java
   import java.nio.file.*;
   import java.util.*;
   import java.util.stream.*;
   import org.apache.spark.sql.*;
   import org.apache.spark.api.java.JavaSparkContext;
   import org.apache.hudi.client.SparkRDDWriteClient;
   import org.apache.hudi.client.common.HoodieSparkEngineContext;
   import org.apache.hudi.common.table.HoodieTableMetaClient;
   import org.apache.hudi.common.table.HoodieTableConfig;
   import org.apache.hudi.common.table.TableSchemaResolver;
   import org.apache.hudi.common.util.Option;
   import org.apache.hudi.config.HoodieWriteConfig;
   import org.apache.hudi.hadoop.fs.HadoopFSUtils;
   import org.apache.hudi.table.action.HoodieWriteMetadata;
   
   public class CdcLogCompactionRepro {
     static SparkSession spark;
     static JavaSparkContext jsc;
     static Path root;
     static int validatedCases;
     public static void main(String[] args) throws Exception {
       TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
       root = Files.createTempDirectory(Paths.get("evidence"), 
"tables-").toAbsolutePath();
       System.out.println("EVIDENCE_ROOT=" + root);
       System.out.println("HUDI_RUNTIME=" + 
SparkRDDWriteClient.class.getProtectionDomain().getCodeSource().getLocation());
       spark = 
SparkSession.builder().master("local[2]").appName("hudi-cdc-logcompaction-repro")
           .config("spark.driver.host", 
"127.0.0.1").config("spark.driver.bindAddress", "127.0.0.1")
           .config("spark.ui.enabled", 
"false").config("spark.sql.shuffle.partitions", "1")
           .config("spark.default.parallelism", "1").config("spark.serializer", 
"org.apache.spark.serializer.KryoSerializer")
           .config("spark.sql.extensions", 
"org.apache.spark.sql.hudi.HoodieSparkSessionExtension")
           .config("spark.sql.catalog.spark_catalog", 
"org.apache.spark.sql.hudi.catalog.HoodieCatalog")
           .config("spark.sql.adaptive.enabled", "false").getOrCreate();
       spark.sparkContext().setLogLevel("ERROR");
       jsc = JavaSparkContext.fromSparkContext(spark.sparkContext());
       try {
         runCase("cdc_disabled", false, false);
         runCase("cdc_enabled_logs", true, false);
         runCase("cdc_enabled_after_compaction", true, true);
         runCase("cdc_disabled_threshold10", false, true);
         runCase("cdc_enabled_no_blocks_threshold10", true, false);
         runCase("cdc_blocks_threshold10", true, true);
         runCase("cdc_data_before_threshold10", true, true);
         runCase("cdc_op_key_only_threshold10", true, true);
         runCase("cdc_flag_disabled_existing_blocks_threshold10", true, true);
         runCase("sparse_cdc_disabled_threshold10", false, true);
         runCase("sparse_cdc_blocks_threshold10", true, true);
         runCase("sparse_cdc_data_before_threshold10", true, true);
         runCase("sparse_cdc_op_key_only_threshold10", true, true);
         runCase("sparse_cdc_flag_disabled_existing_blocks_threshold10", true, 
true);
         if (validatedCases != 14) throw new AssertionError("Missing results: " 
+ validatedCases);
         System.out.println("REPRODUCTION_VERIFIED cases=" + validatedCases + " 
expected_NPE=4");
       } finally { spark.stop(); }
     }
     static Map<String,String> options(String name, boolean cdc) {
       Map<String,String> p = new HashMap<>();
       p.put("hoodie.table.name", name);
       p.put("hoodie.datasource.write.table.type", "MERGE_ON_READ");
       p.put("hoodie.datasource.write.recordkey.field", "id");
       p.put("hoodie.datasource.write.precombine.field", "ts");
       p.put("hoodie.datasource.write.partitionpath.field", "part");
       p.put("hoodie.table.cdc.enabled", String.valueOf(cdc));
       p.put("hoodie.table.cdc.supplemental.logging.mode", "data_before_after");
       p.put("hoodie.metadata.enable", "false");
       p.put("hoodie.compact.inline", "false");
       p.put("hoodie.compact.schedule.inline", "false");
       p.put("hoodie.compact.inline.max.delta.commits", "1");
       p.put("hoodie.clean.automatic", "false");
       p.put("hoodie.archive.automatic", "false");
       p.put("hoodie.log.compaction.enable", "false");
       p.put("hoodie.log.compaction.blocks.threshold", "3");
       p.put("hoodie.embed.timeline.server", "false");
       p.put("hoodie.write.markers.type", "DIRECT");
       p.put("hoodie.insert.shuffle.parallelism", "1");
       p.put("hoodie.upsert.shuffle.parallelism", "1");
       p.put("hoodie.bulkinsert.shuffle.parallelism", "1");
       p.put("hoodie.delete.shuffle.parallelism", "1");
       return p;
     }
     static void write(String path, Map<String,String> opts, int n) {
       Dataset<Row> df = spark.read().json(spark.createDataset(Arrays.asList(
           "{\"id\":\"a\",\"ts\":" + n + ",\"value\":\"a" + n + 
"\",\"part\":\"p\"}",
           "{\"id\":\"b\",\"ts\":" + n + ",\"value\":\"b" + n + 
"\",\"part\":\"p\"}"), Encoders.STRING()));
       df.coalesce(1).write().format("hudi").options(opts)
           .option("hoodie.datasource.write.operation", n == 0 ? "bulk_insert" 
: "upsert")
           .mode(SaveMode.Append).save(path);
     }
     static HoodieTableMetaClient meta(String path) {
       return 
HoodieTableMetaClient.builder().setConf(HadoopFSUtils.getStorageConf(jsc.hadoopConfiguration()))
           .setBasePath(path).setLoadActiveTimelineOnLoad(true).build();
     }
     static SparkRDDWriteClient client(String path, Map<String,String> opts) 
throws Exception {
       Properties p = new Properties(); p.putAll(opts);
       HoodieWriteConfig cfg = 
HoodieWriteConfig.newBuilder().withPath(path).forTable(opts.get("hoodie.table.name"))
           .withSchema(new 
TableSchemaResolver(meta(path)).getTableSchema().toString()).withProps(p).build();
       return new SparkRDDWriteClient(new HoodieSparkEngineContext(jsc), cfg);
     }
     static List<String> snapshot(String path) {
       return 
spark.read().format("hudi").load(path).select("id","ts","value","part").orderBy("id").toJSON().collectAsList();
     }
     static List<String> cdcRows(String path) {
       List<String> rows = new ArrayList<>(spark.read().format("hudi")
           .option("hoodie.datasource.query.type", "incremental")
           .option("hoodie.datasource.query.incremental.format", "cdc")
           .option("hoodie.datasource.read.begin.instanttime", "000")
           .load(path).toJSON().collectAsList());
       Collections.sort(rows);
       return rows;
     }
     static boolean hasNpe(Throwable t) {
       for (Throwable c=t;c!=null;c=c.getCause()) {
         if (c instanceof NullPointerException && 
c.getMessage().contains("non-null")) return true;
       }
       return false;
     }
     static void runCase(String name, boolean cdc, boolean compactFirst) throws 
Exception {
       System.out.println("CASE_BEGIN=" + name);
       String path = root.resolve(name).toString();
       Map<String,String> opts = options(name, cdc);
       if (name.endsWith("threshold10")) 
opts.put("hoodie.log.compaction.blocks.threshold", "10");
       if (name.contains("data_before_threshold")) 
opts.put("hoodie.table.cdc.supplemental.logging.mode", "data_before");
       if (name.contains("op_key_only")) 
opts.put("hoodie.table.cdc.supplemental.logging.mode", "op_key_only");
       write(path, opts, 0);
       for (int n=1;n<=3;n++) write(path, opts, n);
       if (compactFirst) {
         try (SparkRDDWriteClient c = client(path, opts)) {
           Option<String> instant = c.scheduleCompaction(Option.empty());
           if (!instant.isPresent()) throw new AssertionError("No regular 
compaction scheduled");
           HoodieWriteMetadata result = c.compact(instant.get());
           c.commitCompaction(instant.get(), result, Option.empty());
           System.out.println("REGULAR_COMPACTION=" + instant.get());
         }
         for (int n=4;n<=6;n++) write(path, opts, n);
       }
       boolean sparse = name.startsWith("sparse_");
       if (sparse) {
         for (int n=7;n<=13;n++) {
           Dataset<Row> extra = 
spark.read().json(spark.createDataset(Collections.singletonList(
               "{\"id\":\"z\",\"ts\":" + n + ",\"value\":\"z" + n + 
"\",\"part\":\"q\"}"), Encoders.STRING()));
           extra.coalesce(1).write().format("hudi").options(opts)
               .option("hoodie.datasource.write.operation", 
"upsert").mode(SaveMode.Append).save(path);
         }
         System.out.println("SPARSE_FIXTURE=10 delta commits after regular 
compaction; only 3 update partition p");
       }
       List<String> before = snapshot(path);
       int expectedTs = compactFirst ? 6 : 3;
       List<String> expected = Arrays.asList(
           "{\"id\":\"a\",\"ts\":" + expectedTs + ",\"value\":\"a" + expectedTs 
+ "\",\"part\":\"p\"}",
           "{\"id\":\"b\",\"ts\":" + expectedTs + ",\"value\":\"b" + expectedTs 
+ "\",\"part\":\"p\"}");
       if (sparse) {
         expected = new ArrayList<>(expected);
         
expected.add("{\"id\":\"z\",\"ts\":13,\"value\":\"z13\",\"part\":\"q\"}");
       }
       if (!before.equals(expected)) throw new AssertionError("Unexpected 
fixture snapshot " + before);
       boolean disableCdcFlag = name.contains("flag_disabled");
       List<String> cdcBefore = cdc && !disableCdcFlag ? cdcRows(path) : null;
       if (cdcBefore != null) System.out.println("CDC_ROWS_BEFORE=" + 
cdcBefore.size());
       if (disableCdcFlag) {
         HoodieTableMetaClient m = meta(path);
         Properties changed = new Properties(); 
changed.setProperty("hoodie.table.cdc.enabled", "false");
         HoodieTableConfig.update(m.getStorage(), m.getMetaPath(), changed);
         opts.put("hoodie.table.cdc.enabled", "false");
         if (meta(path).getTableConfig().isCDCEnabled()) throw new 
AssertionError("CDC flag still enabled");
         System.out.println("CDC_FLAG_DISABLED retaining existing CDC log");
       }
       boolean expectNpe = sparse && cdc;
       long cdcFiles;
       System.out.println("SNAPSHOT_BEFORE=" + before);
       try (Stream<Path> paths = Files.walk(Paths.get(path))) {
         List<Path> logs = paths.filter(p -> 
p.getFileName().toString().contains(".log.") && 
!p.toString().endsWith(".crc")).collect(Collectors.toList());
         cdcFiles = logs.stream().filter(p -> 
p.toString().endsWith(".cdc")).count();
         logs.forEach(p -> System.out.println("LOG_FILE=" + p));
       }
       if ((cdc && compactFirst) != (cdcFiles > 0)) throw new 
AssertionError("Unexpected CDC file count " + cdcFiles);
       String stage = "schedule";
       try (SparkRDDWriteClient c = client(path, opts)) {
         Option<String> instant = c.scheduleLogCompaction(Option.empty());
         if (!instant.isPresent()) {
           if (expectNpe || !name.endsWith("threshold10")) throw new 
AssertionError("Unexpected unscheduled log compaction");
           if (!before.equals(snapshot(path))) throw new 
AssertionError("Snapshot changed");
           if (cdcBefore != null && !cdcBefore.equals(cdcRows(path))) throw new 
AssertionError("CDC changed");
           System.out.println("CASE_RESULT=" + name + " NO_OP below threshold; 
snapshot unchanged=" + before.equals(snapshot(path)));
           validatedCases++;
           return;
         }
         System.out.println("LOG_COMPACTION_SCHEDULED=" + instant.get());
         stage = "execute";
         HoodieWriteMetadata result = c.logCompact(instant.get());
         c.commitLogCompaction(instant.get(), result, Option.empty());
         stage = "verify";
         List<String> after = snapshot(path);
         if (!before.equals(after)) throw new AssertionError("Snapshot changed: 
" + after);
         if (expectNpe) throw new AssertionError("Expected reproduction 
missing");
         if (cdcBefore != null && !cdcBefore.equals(cdcRows(path))) throw new 
AssertionError("CDC changed after log compaction");
         System.out.println("CASE_RESULT=" + name + " PASS snapshot unchanged 
rows=" + after.size());
         validatedCases++;
       } catch (Throwable t) {
         System.out.println("CASE_RESULT=" + name + " FAIL stage=" + stage);
         t.printStackTrace(System.out);
         List<String> after = snapshot(path);
         System.out.println("SNAPSHOT_AFTER_FAILURE=" + after);
         if (!expectNpe || !stage.equals("schedule") || !hasNpe(t)) throw new 
AssertionError("Unexpected failure", t);
         if (!before.equals(after)) throw new AssertionError("Snapshot changed 
after failure");
         if (cdcBefore != null && !cdcBefore.equals(cdcRows(path))) throw new 
AssertionError("CDC changed after failure");
         System.out.println("EXPECTED_NPE_VERIFIED=" + name + " 
snapshot_and_available_CDC_unchanged");
         validatedCases++;
       }
     }
   }
   
   ```
   </details>
   
   <details>
   <summary>Observed case results</summary>
   
   ```text
   CASE_RESULT=cdc_disabled PASS snapshot unchanged rows=2
   CASE_RESULT=cdc_enabled_logs PASS snapshot unchanged rows=2
   CASE_RESULT=cdc_enabled_after_compaction PASS snapshot unchanged rows=2
   CASE_RESULT=cdc_disabled_threshold10 NO_OP below threshold; snapshot 
unchanged=true
   CASE_RESULT=cdc_enabled_no_blocks_threshold10 NO_OP below threshold; 
snapshot unchanged=true
   CASE_RESULT=cdc_blocks_threshold10 NO_OP below threshold; snapshot 
unchanged=true
   CASE_RESULT=cdc_data_before_threshold10 NO_OP below threshold; snapshot 
unchanged=true
   CASE_RESULT=cdc_op_key_only_threshold10 NO_OP below threshold; snapshot 
unchanged=true
   CASE_RESULT=cdc_flag_disabled_existing_blocks_threshold10 NO_OP below 
threshold; snapshot unchanged=true
   CASE_RESULT=sparse_cdc_disabled_threshold10 NO_OP below threshold; snapshot 
unchanged=true
   CASE_RESULT=sparse_cdc_blocks_threshold10 FAIL stage=schedule
   EXPECTED_NPE_VERIFIED=sparse_cdc_blocks_threshold10 
snapshot_and_available_CDC_unchanged
   CASE_RESULT=sparse_cdc_data_before_threshold10 FAIL stage=schedule
   EXPECTED_NPE_VERIFIED=sparse_cdc_data_before_threshold10 
snapshot_and_available_CDC_unchanged
   CASE_RESULT=sparse_cdc_op_key_only_threshold10 FAIL stage=schedule
   EXPECTED_NPE_VERIFIED=sparse_cdc_op_key_only_threshold10 
snapshot_and_available_CDC_unchanged
   CASE_RESULT=sparse_cdc_flag_disabled_existing_blocks_threshold10 FAIL 
stage=schedule
   EXPECTED_NPE_VERIFIED=sparse_cdc_flag_disabled_existing_blocks_threshold10 
snapshot_and_available_CDC_unchanged
   REPRODUCTION_VERIFIED cases=14 expected_NPE=4
   ```
   </details>
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to