nsivabalan commented on PR #19946:
URL: https://github.com/apache/hudi/pull/19946#issuecomment-6070719946

   # Review draft — apache/hudi#19946
   
   ## Mergeability summary
   
   - Narrow, well-scoped change: 5 lines of production code reusing an existing 
predicate. No new algorithm, no new state, no config.
   - No breaking changes. No storage format, public API, config semantics, wire 
protocol, or file-selection changes.
   - The optimization is confined to the mismatch the final consistency check 
already tolerates, so response behavior is unchanged for every other case.
   - The symmetry is the strongest property here: the sync decision and the 
reject decision now come from the same function on the same (unfiltered) 
timeline, so they cannot drift.
   - Test coverage is thorough — 23 tests including concurrent stale clients, 
file-backed operation counts, deleted-commit recovery, and local-fallback 
preservation.
   - History shows the broader version of this optimization was tried 
(81ba62a), P1'd, and broke `TestSavepointRestoreMergeOnRead` in CI. The current 
narrow form is the right landing spot.
   
   **Recommendation: approve**, with one non-blocking ordering suggestion.
   
   ## Breaking changes
   
   None.
   
   ## Findings
   
   ### SUGGESTION — hoist the predicate above `isLocalViewBehind` (sync site 
only)
   
   `RequestHandler.java:770-780`. Current shape enters a block named "view is 
behind" and then returns without syncing, which reads as a bug. Inverting also 
avoids wasted work: `isLocalViewBehind` builds a filtered timeline and computes 
a SHA-256 over the full instant list (`:742`, `:752`), while 
`shouldThrowExceptionIfLocalViewBehind` only inspects `lastInstant()`. In the 
trailing-clean case the current order pays for the hash and discards the answer.
   
   ```java
   HoodieTimeline localTimeline = view.getTimeline();
   if (!shouldThrowExceptionIfLocalViewBehind(localTimeline, 
getTimelineHashParam(ctx))) {
     return false;
   }
   if (isLocalViewBehind(ctx)) {
     ...
     view.sync();
     return true;
   }
   ```
   
   **Important caveat: do not mirror this at the final check (`:692-702`).** 
The two predicates read different timelines — `isLocalViewBehind` uses 
`getTimeline().filterCompletedOrMajorOrMinorCompactionInstants()` (`:742`), 
while `shouldThrowExceptionIfLocalViewBehind` uses the unfiltered 
`getTimeline()` (`:695-696`). `shouldThrow...` returns `true` for essentially 
every healthy request whose last instant is not a clean, so promoting it to the 
outer condition there would 400 on matching timelines. `isLocalViewBehind` is 
the guard that gives it meaning.
   
   ### SUGGESTION — note the filter asymmetry
   
   Pre-existing, not introduced here, but the new call site inherits it: the 
"detect" step hashes a filtered timeline and the "forgive" step hashes an 
unfiltered one. Worth a comment so the next reader does not assume they are 
interchangeable. `testCompactionStateAndFiltering` already covers the 
log-compaction case where the two disagree. Aligning both on one filtered 
timeline would be cleaner but has its own blast radius and should not ride 
along on this PR.
   
   ### NIT — method name
   
   `shouldThrowExceptionIfLocalViewBehind` now also gates refreshes, so the 
name undersells it. Something like `isBenignTimelineDifference` would read 
better at both call sites. Not worth churn if there is no other reason to touch 
the method.
   
   ## On the multi-clean discussion
   
   Two reviewers are currently pulling in opposite directions and neither has 
replied to the other:
   
   - @prashantwason (2026-10-08) proposes extending the exemption to walk back 
over all trailing cleans, with a reproducing test.
   - @danny0405 (2026-10-08) asked for exactly that implementation to be 
reverted as "a rare case" — which the author did in `e2800c2` / `c7c713d`.
   
   Worth resolving explicitly rather than leaving the next reader to guess. The 
narrow form is defensible: a second concurrent clean completing inside one 
stage is genuinely uncommon, and the complexity is real. If the production 
evidence says otherwise, that is the argument to make.
   
   ## Suggested follow-up (not for this PR)
   
   The remaining cost is the general stale-client case: when a concurrent 
writer leaves N executors pinned to an older timeline, every request reloads, 
each reload lands on the same storage state, and nothing converges — reloads 
are only useful when the server is behind storage. This is the trailing-clean 
problem's general form and is not addressed here.
   
   I have opened #20239 for that, which carries the commits from this PR so it 
applies to master on its own. The server records the client hash and last known 
instant a reload was performed for, together with the server timeline hash that 
reload produced, and skips the reload while that server timeline is unchanged. 
Only an observed reload counts as evidence — the client hash is never 
interpreted as proof the server is ahead, which is what made 81ba62a unsafe 
here. Validated with all 107 timeline-service tests and 
`TestSavepointRestoreMergeOnRead` at 18/18, the suite that failed 4/4 on 
81ba62a, with no existing test modified.
   
   If this PR merges first, #20239 rebases down to just the one commit. Happy 
to close it in favour of folding the change in here instead, whichever the 
reviewers prefer.
   


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