voonhous commented on code in PR #19369:
URL: https://github.com/apache/hudi/pull/19369#discussion_r3802700818


##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/TimelineServerBasedWriteMarkers.java:
##########
@@ -168,7 +170,8 @@ public Option<StoragePath> 
createWithEarlyConflictDetection(String partitionPath
    * @param markerFileName Marker file name.
    * @return {@code true} if successful; {@code false} otherwise.
    */
-  private boolean executeCreateMarkerRequest(Map<String, String> paramsMap, 
String partitionPath, String markerFileName) {
+  @VisibleForTesting
+  boolean executeCreateMarkerRequest(Map<String, String> paramsMap, String 
partitionPath, String markerFileName) {

Review Comment:
   `executeCreateMarkerRequest` hands back a bare boolean, and that boolean 
carries three distinct outcomes: created, already exists, and 
early-conflict-detected. Collapsing them here is what lets the new throw above 
fire for a conflict, and the exception type matters.
   
   `createWithEarlyConflictDetection` in this same file throws 
`HoodieEarlyConflictDetectionException` for the identical `false`, and 
`HoodieAppendHandle` keys off exactly that type:
   
   ```java
   if (!config.getIgnoreWriteFailed() || ExceptionUtil.isCausedBy(e, 
HoodieEarlyConflictDetectionException.class)) {
     throw new HoodieException(e.getMessage(), e);
   }
   ```
   
   `hoodie.write.ignore.failed` defaults to `true` 
(`HoodieWriteConfig.IGNORE_FAILED`), so a conflict that arrives as 
`HoodieIOException` is swallowed into `writeStatus.markFailure` instead of 
aborting the write. This is reachable: with server-side early conflict 
detection enabled, `MarkerHandler#createMarker` runs detection for every create 
on that marker dir, and `WriteMarkers` routes pending compaction and clustering 
instants through the plain `create(..., false)` path even under OCC.
   
   Concrete suggestion: return the outcome from the server rather than a 
boolean, and map each one separately -- `Option.of` for created, 
`Option.empty()` for already-exists when `checkIfExists` is set, and 
`HoodieEarlyConflictDetectionException` for a detected conflict. If you would 
rather keep the wire format as-is for now, at minimum do not convert a conflict 
into `HoodieIOException`.



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/TimelineServerBasedWriteMarkers.java:
##########
@@ -137,7 +139,7 @@ protected Option<StoragePath> create(String partitionPath, 
String fileName, IOTy
     if (success) {
       return Option.of(new 
StoragePath(FSUtils.constructAbsolutePath(markerDirPath, partitionPath), 
markerFileName));
     } else {
-      return Option.empty();
+      throw new HoodieIOException("[timeline-server-based] Failed to create 
marker for partition " + partitionPath + ", fileName " + fileName + " with 
IOType " + type);

Review Comment:
   Digging into the server side, `false` has exactly two producers, and neither 
one is a failed create:
   
   - `MarkerDirState#processMarkerCreationRequests` -> 
`future.setIsSuccessful(!exists)`, i.e. the marker already exists.
   - the `catch (HoodieEarlyConflictDetectionException)` in that same method, 
plus the equivalent in `MarkerHandler#createMarker` via 
`finishCreateMarkerFuture`, i.e. early conflict detection fired.
   
   A genuine failure never reaches the client as `false`. Transport errors 
already become `HoodieRemoteException` inside `executeCreateMarkerRequest`, and 
a marker flush failure throws `HoodieIOException` out of 
`processMarkerCreationRequests` before the futures are completed (that leg has 
since been fixed on master by #19368). So the message on this exception names a 
condition that this branch cannot actually be in.
   
   @nsivabalan on "a client can never ask to create a marker that already 
exists" -- the log-marker path can, and the surrounding code is built to 
tolerate it:
   
   - `HoodieWriteHandle#getLogCreationCallback` returns 
`createLogMarkerIfNotExists(...).isPresent()`, and all three consumers discard 
that boolean (`HoodieLogFormatWriter#createNewFile`, 
`HoodieNativeLogFormatWriter`, `HoodieNativeCDCFileWriter`). An empty Option 
was designed as a tolerated no-op.
   - `HoodieLogFormatWriter#getOutputStream` is an explicit collision-recovery 
loop: `while (!created) { if (exists(logFile)) rollOver(); createNewFile(); }` 
with `catch (FileAlreadyExistsException) { rollOver(); }`. `createNewFile()` 
fires `preFileCreation`, i.e. creates the marker, *before* 
`storage.create(path, false)`. A task that dies in that window leaves a marker 
with no log file behind it; the retry recomputes the same rolled-over name and, 
with this change, throws on that attempt and every later one, because the 
marker persists in `.hoodie/.temp/<instant>` for the life of the instant. Two 
writers racing the rollover hit the same thing: the loser used to recover 
through `FileAlreadyExistsException -> rollOver`.
   - The write token is not always distinct per attempt. `HoodieWriteHandle` 
inherits `latestLogFile.getLogWriteToken()` for table version below 8, 
`FSUtils.makeWriteToken` falls back to the constant 
`HoodieLogFormat.DEFAULT_WRITE_TOKEN` ("0-0-0") when the task context is 
unavailable, and `JavaTaskContextSupplier` returns 0/0/0 unconditionally.
   - `RollbackHelperV1` uses `createIfNotExists(..., IOType.APPEND, ...)` for 
rollback log appends; a retried rollback reuses the same rollback instant, so 
the same marker dir and the same log file names.
   
   Concrete suggestion: gate the throw on `checkIfExists`, so 
`createIfNotExists` keeps returning `Option.empty()`. That matches 
`DirectWriteMarkers.create`, which returns empty for already-exists and 
reserves a throw for real I/O failure, and it matches the contract written on 
the method being overridden -- `WriteMarkers#create(String, String, IOType, 
boolean)`: "the marker path or empty option if already exists and 
`checkIfExists` is true". The more complete fix is to have the server report 
which of the three outcomes happened, since a bare boolean cannot express it.



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