deepakpanda93 commented on issue #19693:
URL: https://github.com/apache/hudi/issues/19693#issuecomment-5414829269

   We reproduced this end-to-end in a local Spark 3.5 environment, and 
validated the workaround. Short answers first, then the evidence.
   
   1. **Does `USE_TRANSITION_TIME` fix the hollow commit error?** Yes — the 
error goes away and no data is lost.
   2. **Is there a risk of data loss?** No, but there is a correctness catch in 
the other direction: it silently **re-reads already-consumed commits** unless 
you also change what you store as `extractId`.
   3. **Can you switch without regenerating your lake?** Yes — no regeneration 
needed. The only thing that changes is your checkpoint value, not your data.
   
   ---
   
   ## What we ran
   
   * Spark 3.5.7, local filesystem, MOR table, 4 commits (`ids 1-10`, `11-20`, 
`21-30`, `31-40`).
   * Hollow commit created exactly as in your step 3 — a stuck 
`<ts>.deltacommit.requested` on the timeline, positioned between commit 2 and 
commit 3 so completed commits exist after it. It has no matching `.inflight` or 
completed file and was present for every read below.
   * Incremental read with `begin.instanttime` = commit 1, so a fully correct 
answer is **ids 11-40 (30 rows)**.
   
   **Version note:** Hudi 0.14.1 has no open-source Spark 3.5 bundle 
(`hudi-spark3.5-bundle_2.12` starts at 0.15.0 — EMR 7.1.0 ships Amazon's own 
build), so we used **0.15.1**. Before substituting we diffed 
`TimelineUtils.handleHollowCommitIfNeeded` between `release-0.14.1` and 
`release-0.15.1`: the only difference is a `String.format` → 
parameterized-SLF4J logging change. The logic is identical.
   
   ## Result on the 0.14.x / 0.15.x code path (table version 6)
   
   | `hoodie.read.timeline.holes.resolution.policy` | Result |
   |---|---|
   | *(default)* | **ERROR** — `Found hollow commit: '20260825173626458'` |
   | `FAIL` | same error |
   | `BLOCK` | OK — **10 rows, ids 11-20** |
   | `USE_TRANSITION_TIME` | OK — **40 rows, ids 1-40** |
   
   This confirms your reading of the options. `BLOCK` truncates the range at 
the hollow commit, so nothing past it flows until the stuck producer finishes — 
the multi-hour stall you cannot afford.
   
   ## The catch with `USE_TRANSITION_TIME`
   
   It returned **40 rows, not the expected 30**. Nothing was lost — batch 1 was 
re-delivered.
   
   With this policy, `begin.instanttime` stops meaning *instant time* and 
starts meaning *completion time*. Every commit completes strictly after its 
instant time, so an instant-time checkpoint pulls the previous commit back into 
range.
   
   ## Verified workaround for 0.14.1
   
   Set the policy **and** switch `extractId` to a completion time in the same 
change. On table version 6 the completion time is not in the instant filename 
(`20260825173621649.deltacommit`) — it is the instant file's modification time, 
exposed as `HoodieInstant.getStateTransitionTime()`. This ran successfully 
against 0.15.1:
   
   ```python
   jvm   = spark._jvm
   hconf = spark._jsc.hadoopConfiguration()
   
   storage_conf = 
jvm.org.apache.hudi.hadoop.fs.HadoopFSUtils.getStorageConf(hconf)
   mc = (jvm.org.apache.hudi.common.table.HoodieTableMetaClient.builder()
           .setConf(storage_conf).setBasePath(TABLE_PATH).build())
   
   instants = list(mc.getActiveTimeline().getCommitsTimeline()
                     .filterCompletedInstants().getInstants())
   
   # instant time -> completion time
   for i in instants:
       print(i.getTimestamp(), "->", i.getStateTransitionTime())
   
   next_extract_id = max(i.getStateTransitionTime() for i in instants)
   ```
   
   Producing, on our table:
   
   ```
   20260825173621649 -> 20260825173625078
   20260825173626457 -> 20260825173628563
   20260825173629821 -> 20260825173630326
   20260825173631586 -> 20260825173632003
   ```
   
   Then read with that value:
   
   ```python
   (spark.read.format("hudi")
      .option("hoodie.datasource.query.type", "incremental")
      .option("hoodie.read.timeline.holes.resolution.policy", 
"USE_TRANSITION_TIME")
      .option("hoodie.datasource.read.begin.instanttime", next_extract_id)
      .load(TABLE_PATH))
   ```
   
   Measured results, with the hollow commit still stuck on the timeline:
   
   | `begin.instanttime` | Rows |
   |---|---|
   | C1 **instant** time `20260825173621649` | 40 rows, ids 1-40 — re-reads 
batch 1 |
   | C1 **completion** time `20260825173625078` | **30 rows, ids 11-40** — 
correct |
   | max completion `20260825173632003` (next run) | **0 rows** — clean steady 
state |
   
   The last row is the important one: the checkpoint advances and converges, so 
consecutive runs neither stall nor re-read.
   
   For the cutover run, take the current max completion time as your new 
`extractId` baseline. No table rewrite or regeneration is required.
   
   ## On Hudi 1.x this failure mode does not exist
   
   Incremental reads select their implementation by table version 
(`DefaultSource.scala`): table version >= 8 routes to the **V2** incremental 
relation, which is completion-time native and never consults hollow-commit 
handling — there is no `HollowCommitHandling` reference anywhere in 
`IncrementalRelationV2` / `MergeOnReadIncrementalRelationV2`.
   
   On **1.2.0**, with the identical hollow commit in place, **all four policy 
values returned 40 rows with no error.** The config is inert there.
   
   We also verified your "no data loss between consecutive runs" requirement on 
1.2.0 with the hollow commit present:
   
   * **Run A** from commit 1's completion time -> 30 rows, ids 11-40 (spanned 
the hollow commit, no error, no blocking)
   * producer writes batch 5 (ids 41-50)
   * **Run B** from commit 4's completion time -> 10 rows, ids 41-50
   * Across ids 11-50: **`missing=[]`, `duplicated=[]`**
   
   Table version 8 also puts the completion time directly in the instant 
filename as `<requestedTime>_<completionTime>.deltacommit`, so no timeline API 
call is needed to maintain the checkpoint. **Upgrading to 1.x is the cleaner 
fix** — it removes the failure mode instead of working around it, and 
`USE_TRANSITION_TIME` becomes unnecessary.
   
   ## Caveats on our reproduction
   
   * The hollow commit was created by writing the `.deltacommit.requested` file 
directly. A fabricated instant carries no heartbeat, so Hudi eventually 
reclaims it: a continuity attempt failed with `HoodieRollbackException: Found 
commits after time ..., please rollback greater commits first` under the 
default EAGER failed-write cleanup, and even with 
`hoodie.cleaner.policy.failed.writes=LAZY` the instant was gone by the 
following write. A genuinely running producer holds a heartbeat and stays 
in-flight. This affects how long the simulated hollow state survives, not the 
read results above, all of which were measured while it was present.
   * Local filesystem, single `local[2]` Spark — the cloud-storage rate 
limiting you see in production is not exercised here.


-- 
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