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

   ### Bug Description
   
   **What happened:**
   
   Log compaction changes `_hoodie_commit_time` on unchanged MOR records to the 
log-compaction instant. On Hudi **1.2.1**, an incremental query restricted to 
that service-only window initially returns zero rows, but returns an unchanged 
row after a later regular compaction and cleaning force incremental full-table 
fallback. This causes duplicate downstream processing even though the window 
contains no logical writes.
   
   Also reproduced on **1.1.0**, where the duplication occurs immediately after 
log compaction as well. The 1.2.1 improvement before cleaning does not fully 
resolve the problem.
   
   **What you expected:**
   
   Log compaction should preserve original record commit metadata. A window 
containing only log compaction should not produce changed rows, including when 
the incremental reader falls back after cleaning. A later normal write should 
return only that genuinely changed record.
   
   **Steps to reproduce:**
   
   1. Create a fresh MOR table with fields `id`, `ts`, `value`, `part`; 
bulk-insert `a` and `b` in partition `p`.
   2. Update both records in three separate commits. Their final values are 
`a3` and `b3` with `ts=3`. Read and consume these changes.
   3. Explicitly schedule, execute and commit log compaction with block 
threshold 3. No logical write occurs in this service. Save its requested time L 
and completion time C.
   4. Query incremental `(L, C]`. L is after all data-write completions and 
before service completion, so this window contains only the log-compaction 
commit. On 1.2.1 it returns zero rows; on 1.1.0 it returns both old rows. 
Snapshot metadata now reports `_hoodie_commit_time=L` on both records, although 
values are unchanged.
   5. Upsert **only a** to `a4`, `ts=4`, in a later commit.
   6. Explicitly schedule, execute and commit regular compaction.
   7. Run the cleaner with `KEEP_LATEST_FILE_VERSIONS`, retaining one file 
version. Verify the original base file has been removed and the latest 
compacted base file remains.
   8. Repeat the **same** incremental `(L, C]` query with the default 
full-table fallback. Both 1.1.0 and **1.2.1 return b3 with 
`_hoodie_commit_time=L`**, even though b was never modified in that window. The 
last real write to b predates L.
   
   The cleaner uses a supported policy through `SparkRDDWriteClient.clean()`; 
no files/timeline entries are manually deleted or modified. The intentionally 
short retention makes the delayed-consumer/full-table-fallback case easy to 
reproduce.
   
   **Results:**
   
   | Window | 1.1.0 | 1.2.1 |
   |---|---|---|
   | Log compaction only, immediately | 2 duplicate rows | 0 rows |
   | Log compaction + later update to a | a4 + duplicate b3 | a4 only |
   | Later write only | a4 only | a4 only |
   | Regular compaction only | 0 rows | 0 rows |
   | Historical log-compaction window after regular compaction, before clean | 
2 duplicate rows | 0 rows |
   | Same historical window after clean | **duplicate b3** | **duplicate b3** |
   
   Results match with CDC disabled and enabled (`data_before_after`), and with 
`hoodie.file.group.reader.enabled` omitted, true, or false. Additionally 
reproduced using 1.2.1 writing a fresh **table-version-8** table, as well as 
its default table version 9. This is not an in-place upgrade test.
   
   There were **44 incremental queries per configuration**, 132 total across 
1.1.0, 1.2.1/v9, and 1.2.1/v8. Snapshot business values remained unchanged by 
both compaction services. No data loss or production-scale performance claim is 
made.
   
   **Related metadata observation:** The service remains a completed 
`deltacommit` with `operation=LOG_COMPACT` and nonzero `numWrites`. Consumers 
that blindly sum those physical-write statistics can also overcount logical 
input. That integration concern is distinct from the incorrect rows returned by 
the incremental reader.
   
   Duplicate searches for log compaction/incremental/clean, log 
compaction/_hoodie_commit_time, and logcompaction/duplicate found no matching 
current report. #20325 concerns a CDC scheduling NPE; this report concerns 
successful log compaction followed by incorrect incremental output.
   
   
   ### Environment
   
   **Hudi versions:** unmodified Spark 3.5/Scala 2.12 bundles, 1.1.0 and latest 
release 1.2.1. Fresh table v8 for 1.1.0; 1.2.1 tested with both default v9 and 
explicitly configured v8.
   **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 
JVM heap; synthetic two-row data, no external catalog or cluster.
   
   **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=false
   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=3
   hoodie.embed.timeline.server=false
   hoodie.write.markers.type=DIRECT
   ```
   CDC-on runs use `hoodie.table.cdc.enabled=true` and 
`hoodie.table.cdc.supplemental.logging.mode=data_before_after`. All shuffle 
parallelism values are 1. Services are explicitly scheduled/executed/committed 
through the write client. The final cleaner call uses 
`hoodie.cleaner.policy=KEEP_LATEST_FILE_VERSIONS` and 
`hoodie.cleaner.fileversions.retained=1`. Incremental query format and 
full-table fallback use defaults.
   
   1.2.1 bundle SHA-1 verified against Maven Central: 
`1120807b30891d6e32e4c71512b8f4fbfa2a8d50`.
   
   
   ### Logs and Stack Trace
   
   This is incorrect output, not a thrown exception. Selected 1.2.1 output:
   
   ```text
   QUERY label=cdc_off_data_before reader=default begin=000 
end=20261014110623942 count=2 
rows=[{"id":"a","ts":3,"value":"a3","part":"p","_hoodie_commit_time":"20261014110623543"},
 
{"id":"b","ts":3,"value":"b3","part":"p","_hoodie_commit_time":"20261014110623543"}]
   
SNAPSHOT_METADATA_AFTER=[{"id":"a","_hoodie_commit_time":"20261014110624974"}, 
{"id":"b","_hoodie_commit_time":"20261014110624974"}]
   TIMELINE requested=20261014110624974 completion=20261014110625352 
action=deltacommit operation=LOG_COMPACT numWrites=2 numUpdates=2
   QUERY label=cdc_off_logcompaction_only reader=default 
begin=20261014110624974 end=20261014110625352 count=0 rows=[]
   QUERY label=cdc_off_checkpoint_boundary reader=default 
begin=20261014110623942 end=20261014110625352 count=0 rows=[]
   QUERY label=cdc_off_new_write_only reader=default begin=20261014110626675 
end=20261014110627053 count=1 
rows=[{"id":"a","ts":4,"value":"a4","part":"p","_hoodie_commit_time":"20261014110626675"}]
   QUERY label=cdc_off_logcompaction_plus_write reader=default 
begin=20261014110624974 end=20261014110627053 count=1 
rows=[{"id":"a","ts":4,"value":"a4","part":"p","_hoodie_commit_time":"20261014110626675"}]
   QUERY label=cdc_off_regular_compaction_only reader=default 
begin=20261014110627951 end=20261014110628270 count=0 rows=[]
   QUERY label=cdc_off_historical_after_regular reader=default 
begin=20261014110624974 end=20261014110625352 count=0 rows=[]
   QUERY label=cdc_off_historical_after_clean reader=default 
begin=20261014110624974 end=20261014110625352 count=1 
rows=[{"id":"b","ts":3,"value":"b3","part":"p","_hoodie_commit_time":"20261014110624974"}]
   
SNAPSHOT_METADATA_AFTER=[{"id":"a","_hoodie_commit_time":"20261014110632061"}, 
{"id":"b","_hoodie_commit_time":"20261014110632061"}]
   TIMELINE requested=20261014110632061 completion=20261014110632394 
action=deltacommit operation=LOG_COMPACT numWrites=2 numUpdates=2
   ```
   
   **Source analysis (not a patched/tested fix):**
   
   In release-1.2.1, [FileGroupReaderBasedAppendHandle's 
constructor](https://github.com/apache/hudi/blob/release-1.2.1/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/io/FileGroupReaderBasedAppendHandle.java#L67)
 calls the ordinary `HoodieAppendHandle` constructor. That path does not set 
the private `isLogCompaction` flag, whereas the older dedicated log-compaction 
constructor does. 
[populateMetadataFields](https://github.com/apache/hudi/blob/release-1.2.1/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/io/HoodieAppendHandle.java#L327)
 therefore replaces record commit time/sequence metadata with the service 
instant. The observed snapshot metadata and later base-file fallback result are 
consistent with this constructor path. Please cover both commit-time 
preservation and delayed incremental reads in a fix.
   
   ### Standalone reproducer
   
   Save the source below as `LogCompactionIncrementalRepro.java`. With Java 11 
and `REPRO_CP` containing Spark SQL + Spark Hive 3.5.6 runtime dependencies, 
commons-logging 1.2, protobuf-java 3.25.5 and exactly one matching Hudi Spark 
3.5/Scala 2.12 bundle:
   
   ```sh
   mkdir -p evidence classes
   javac -cp "$REPRO_CP" -d classes LogCompactionIncrementalRepro.java
   SPARK_LOCAL_IP=127.0.0.1 java -Xmx3g -cp "classes:$REPRO_CP" 
LogCompactionIncrementalRepro
   # For the 1.2.1 table-version-8 variant, add -Drepro.table.version=8 before 
-cp.
   ```
   
   The harness creates disposable local tables under `evidence/` and cleans 
only those tables. It prints observed query rows, service metadata and query 
plans; it checks normal-write and regular-compaction controls and 
business-value preservation. `TEST_COMPLETED` indicates the harness completed; 
a nonzero `duplicate_windows` count records reproduced failures rather than a 
passing correctness test. External checks verified the counts shown in the 
result table for every reader/CDC combination.
   
   <details>
   <summary>Complete Java reproducer</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.TableSchemaResolver;
   import org.apache.hudi.common.table.timeline.HoodieInstant;
   import org.apache.hudi.common.table.timeline.HoodieTimeline;
   import org.apache.hudi.common.model.HoodieCommitMetadata;
   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 LogCompactionIncrementalRepro {
     static SparkSession spark;
     static JavaSparkContext jsc;
     static Path root;
     static int duplicates;
     static int queries;
     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-logcompaction-incremental-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(false); runCase(true); System.out.println("TEST_COMPLETED 
queries="+queries+" duplicate_windows="+duplicates); }
       finally {spark.stop();}
     }
     static String schema(String path) throws Exception {
       TableSchemaResolver resolver=new TableSchemaResolver(meta(path));
       try {return 
resolver.getClass().getMethod("getTableSchema").invoke(resolver).toString();}
       catch (NoSuchMethodException e) {return 
resolver.getClass().getMethod("getTableAvroSchema").invoke(resolver).toString();}
     }
     static Map<String,String> options(String name, boolean cdc) {
       Map<String,String> p = new HashMap<>();
       p.put("hoodie.table.name", name);
       if (System.getProperty("repro.table.version") != null) 
p.put("hoodie.write.table.version",System.getProperty("repro.table.version"));
       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(schema(path)).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 HoodieInstant latest(String path) {
       return 
meta(path).getActiveTimeline().getCommitsTimeline().filterCompletedInstants().lastInstant().get();
     }
     static void timeline(String path) throws Exception {
       HoodieTimeline 
t=meta(path).getActiveTimeline().getCommitsTimeline().filterCompletedInstants();
       for(HoodieInstant 
i:t.getInstantsAsStream().collect(Collectors.toList())) {
         HoodieCommitMetadata m=t.readCommitMetadata(i);
         System.out.println("TIMELINE requested="+i.requestedTime()+" 
completion="+i.getCompletionTime()+" action="+i.getAction()+" 
operation="+m.getOperationType()+" numWrites="+m.fetchTotalRecordsWritten()+" 
numUpdates="+m.fetchTotalUpdateRecordsWritten());
       }
     }
     static List<String> incremental(String path,String begin,String end,String 
reader,String label) throws Exception {
       DataFrameReader 
r=spark.read().format("hudi").option("hoodie.datasource.query.type","incremental")
         
.option("hoodie.datasource.read.begin.instanttime",begin).option("hoodie.datasource.read.end.instanttime",end);
       if(!reader.equals("default")) 
r=r.option("hoodie.file.group.reader.enabled",reader);
       Dataset<Row> df=r.load(path);
       List<String> 
rows=df.select("id","ts","value","part","_hoodie_commit_time").orderBy("id").toJSON().collectAsList();
       queries++;
       System.out.println("QUERY label="+label+" reader="+reader+" 
begin="+begin+" end="+end+" count="+rows.size()+" rows="+rows);
       
Files.writeString(root.resolve(label+"-"+reader+"-plan.txt"),df.queryExecution().toString());
       return rows;
     }
     static void runCase(boolean cdc) throws Exception {
       String name=cdc?"cdc_on":"cdc_off";
       String path=root.resolve(name).toString();
       Map<String,String> opts=options(name,cdc);
       write(path,opts,0);
       for(int n=1;n<=3;n++)write(path,opts,n);
       HoodieInstant data=latest(path);
       List<String> before=snapshot(path);
       List<String> 
dataRows=incremental(path,"000",data.getCompletionTime(),"default",name+"_data_before");
       if(dataRows.size()!=2)throw new AssertionError("Expected 2 original 
rows");
       try(SparkRDDWriteClient c=client(path,opts)) {
         Option<String> i=c.scheduleLogCompaction(Option.empty());
         if(!i.isPresent())throw new AssertionError("Log compaction not 
scheduled");
         HoodieWriteMetadata result=c.logCompact(i.get());
         c.commitLogCompaction(i.get(),result,Option.empty());
       }
       HoodieInstant lc=latest(path);
       if(lc.requestedTime().equals(data.requestedTime()))throw new 
AssertionError("Missing completed log compaction");
       if(!before.equals(snapshot(path)))throw new AssertionError("Snapshot 
changed by log compaction");
       System.out.println("SNAPSHOT_UNCHANGED case="+name);
       
System.out.println("SNAPSHOT_METADATA_AFTER="+spark.read().format("hudi").load(path).select("id","_hoodie_commit_time").orderBy("id").toJSON().collectAsList());
       timeline(path);
       for(String reader:Arrays.asList("default","true","false")) {
         // Start at the service request: after all input writes completed, 
before service completion.
         List<String> 
rows=incremental(path,lc.requestedTime(),lc.getCompletionTime(),reader,name+"_logcompaction_only");
         if(!rows.isEmpty())duplicates++;
         
incremental(path,data.getCompletionTime(),lc.getCompletionTime(),reader,name+"_checkpoint_boundary");
       }
       Dataset<Row> 
update=spark.read().json(spark.createDataset(Collections.singletonList("{\"id\":\"a\",\"ts\":4,\"value\":\"a4\",\"part\":\"p\"}"),Encoders.STRING()));
       
update.coalesce(1).write().format("hudi").options(opts).option("hoodie.datasource.write.operation","upsert").mode(SaveMode.Append).save(path);
       HoodieInstant next=latest(path);
       for(String reader:Arrays.asList("default","true","false")) {
         List<String> 
rows=incremental(path,next.requestedTime(),next.getCompletionTime(),reader,name+"_new_write_only");
         if(rows.size()!=1 || !rows.get(0).contains("a4"))throw new 
AssertionError("New write not read correctly: "+rows);
         List<String> 
mixed=incremental(path,lc.requestedTime(),next.getCompletionTime(),reader,name+"_logcompaction_plus_write");
         if(mixed.size()>1)duplicates++;
       }
       List<String> beforeRegular=snapshot(path);
       try(SparkRDDWriteClient c=client(path,opts)) {
         Option<String> i=c.scheduleCompaction(Option.empty());
         if(!i.isPresent())throw new AssertionError("Regular compaction not 
scheduled");
         HoodieWriteMetadata result=c.compact(i.get()); 
c.commitCompaction(i.get(),result,Option.empty());
       }
       HoodieInstant regular=latest(path);
       if(!beforeRegular.equals(snapshot(path)))throw new 
AssertionError("Snapshot changed by regular compaction");
       for(String reader:Arrays.asList("default","true","false")) {
         List<String> 
rows=incremental(path,regular.requestedTime(),regular.getCompletionTime(),reader,name+"_regular_compaction_only");
         if(!rows.isEmpty())throw new AssertionError("Regular compaction 
control reemits: "+rows);
       }
       for(String reader:Arrays.asList("default","true","false")) {
         List<String> 
historical=incremental(path,lc.requestedTime(),lc.getCompletionTime(),reader,name+"_historical_after_regular");
         if(!historical.isEmpty())duplicates++;
       }
       opts.put("hoodie.cleaner.policy","KEEP_LATEST_FILE_VERSIONS");
       opts.put("hoodie.cleaner.fileversions.retained","1");
       try(SparkRDDWriteClient c=client(path,opts)) { c.clean(); }
       
System.out.println("AFTER_CLEAN_FILES="+Files.walk(Paths.get(path)).filter(p->p.getFileName().toString().endsWith(".parquet")).collect(Collectors.toList()));
       for(String reader:Arrays.asList("default","true","false")) {
         List<String> 
historical=incremental(path,lc.requestedTime(),lc.getCompletionTime(),reader,name+"_historical_after_clean");
         if(!historical.isEmpty())duplicates++;
       }
       System.out.println("CASE_COMPLETED="+name);
     }
   }
   
   ```
   </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