cshuo commented on code in PR #19960:
URL: https://github.com/apache/hudi/pull/19960#discussion_r4069864873


##########
hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/StreamWriteOperatorCoordinator.java:
##########
@@ -412,22 +425,52 @@ public CompletableFuture<CoordinationResponse> 
handleCoordinationRequest(Coordin
   }
 
   private CompletableFuture<CoordinationResponse> 
handleInstantRequest(Correspondent.InstantTimeRequest request) {
-    CompletableFuture<CoordinationResponse> response = new 
CompletableFuture<>();
-    instantRequestExecutor.execute(() -> {
-      long checkpointId = request.getCheckpointId();
-      Pair<String, EventBuffer> instantTimeAndEventBuffer = 
this.eventBuffers.getInstantAndEventBuffer(checkpointId);
-      final String instantTime;
-      if (instantTimeAndEventBuffer == null) {
-        // wait until previous instants are committed.
-        eventBuffers.awaitAllInstantsToCompleteIfNecessary();
-        instantTime = startInstant();
-        this.eventBuffers.initNewEventBuffer(checkpointId, instantTime);
-      } else {
-        instantTime = instantTimeAndEventBuffer.getLeft();
-      }
-      
response.complete(CoordinationResponseSerDe.wrap(Correspondent.InstantTimeResponse.getInstance(instantTime)));
-    }, "request instant time");
-    return response;
+    final long checkpointId = request.getCheckpointId();
+    // Idempotent fast path: the checkpoint -> instant mapping is 
authoritative and survives marker retirement,
+    // so a lost READY reply is recovered by the next poll without creating a 
second instant.
+    Pair<String, EventBuffer> instantTimeAndEventBuffer = 
this.eventBuffers.getInstantAndEventBuffer(checkpointId);
+    if (instantTimeAndEventBuffer != null) {
+      return readyResponse(instantTimeAndEventBuffer.getLeft());
+    }
+    // Atomically submit exactly one creation for this checkpoint. Later polls 
only inspect state.
+    if (instantCreationCheckpoints.add(checkpointId)) {
+      this.instantRequestExecutor.execute(
+          () -> createInstant(checkpointId), "create instant for checkpoint 
%d", checkpointId);
+    }
+
+    // A synchronous test executor may have completed creation already; 
production workers normally return PENDING here.
+    instantTimeAndEventBuffer = 
this.eventBuffers.getInstantAndEventBuffer(checkpointId);
+    if (instantTimeAndEventBuffer != null) {
+      return readyResponse(instantTimeAndEventBuffer.getLeft());
+    }

Review Comment:
   Could we remove this second lookup and return `PENDING` directly? The test 
helper could use `MockCorrespondent.requestInstantTime()` to follow the polling 
protocol. This simplifies the handler at the cost of one extra poll if creation 
finishes immediately.



##########
hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/EventBuffers.java:
##########
@@ -71,7 +72,13 @@ public static EventBuffers getInstance(Configuration conf, 
int dataWriteParallel
   }
 
   public EventBuffer addEventToBuffer(WriteMetadataEvent event) {
-    EventBuffer eventBuffer = 
this.eventBuffers.get(event.getCheckpointId()).getRight();
+    Pair<String, EventBuffer> bufferPair = 
this.eventBuffers.get(event.getCheckpointId());
+    ValidationUtils.checkState(bufferPair != null,
+        "No event buffer bound to checkpoint " + event.getCheckpointId());
+    
ValidationUtils.checkState(bufferPair.getLeft().equals(event.getInstantTime()),
+        String.format("Event instant %s does not match the instant %s bound to 
checkpoint %d",
+            event.getInstantTime(), bufferPair.getLeft(), 
event.getCheckpointId()));

Review Comment:
   Are these defensive validations necessary for this pr?  If the new polling 
flow requires them, could you clarify the scenario that can produce a missing 
buffer or mismatched instant?



##########
hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/event/Correspondent.java:
##########
@@ -64,16 +90,69 @@ public static Correspondent getInstance(OperatorID 
operatorID, TaskOperatorEvent
   }
 
   /**
-   * Sends a request to the coordinator to fetch the instant time.
+   * Requests the instant time for the given checkpoint from the coordinator.
+   *
+   * <p>The coordinator answers each request in O(1) with a {@link Status}: 
the requester polls with a
+   * capped exponential backoff (plus jitter) under a single {@code 
pollBudgetMs} deadline until the
+   * instant is {@code READY}, and retries transient transport errors within 
the same budget. A
+   * {@code PENDING} reply never extends the deadline. Instant creation 
failures fail the job through
+   * the coordinator's normal asynchronous failure path.
+   *
+   * @param checkpointId The checkpoint id (or -1 for bulk insert)
+   * @param pollBudgetMs The overall budget to wait for an instant, in 
milliseconds
+   *
+   * @return the instant time to write with
    */
-  public String requestInstantTime(long checkpointId) {
+  public String requestInstantTime(long checkpointId, long pollBudgetMs) {
+    final long deadlineNanos = System.nanoTime() + 
TimeUnit.MILLISECONDS.toNanos(pollBudgetMs);
+    long backoffMs = POLL_BASE_MS;
+    while (true) {
+      InstantTimeResponse response;
+      try {
+        response = fetchInstantTimeResponse(checkpointId);
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+        throw new HoodieException("Interrupted while requesting the instant 
time from the coordinator", e);
+      } catch (Exception e) {
+        // transient transport/coordinator error: retry within the budget, 
reusing the same checkpoint identity.
+        if (System.nanoTime() >= deadlineNanos) {
+          throw new HoodieException("Timeout requesting the instant time from 
the coordinator for checkpoint " + checkpointId, e);
+        }
+        backoffMs = sleepAndGrow(backoffMs);
+        continue;

Review Comment:
   Could we retry only `PENDING` responses for now and fail fast on exceptions? 
The broad `catch (Exception)` also retries cancellation and deserialization 
failures, which are unlikely to recover through retries. If we want to retry 
transient RPC failures, we should explicitly classify retryable causes after 
unwrapping `ExecutionException`.



##########
hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/event/Correspondent.java:
##########
@@ -64,16 +90,69 @@ public static Correspondent getInstance(OperatorID 
operatorID, TaskOperatorEvent
   }
 
   /**
-   * Sends a request to the coordinator to fetch the instant time.
+   * Requests the instant time for the given checkpoint from the coordinator.
+   *
+   * <p>The coordinator answers each request in O(1) with a {@link Status}: 
the requester polls with a
+   * capped exponential backoff (plus jitter) under a single {@code 
pollBudgetMs} deadline until the
+   * instant is {@code READY}, and retries transient transport errors within 
the same budget. A
+   * {@code PENDING} reply never extends the deadline. Instant creation 
failures fail the job through
+   * the coordinator's normal asynchronous failure path.
+   *
+   * @param checkpointId The checkpoint id (or -1 for bulk insert)
+   * @param pollBudgetMs The overall budget to wait for an instant, in 
milliseconds
+   *
+   * @return the instant time to write with
    */
-  public String requestInstantTime(long checkpointId) {
+  public String requestInstantTime(long checkpointId, long pollBudgetMs) {
+    final long deadlineNanos = System.nanoTime() + 
TimeUnit.MILLISECONDS.toNanos(pollBudgetMs);
+    long backoffMs = POLL_BASE_MS;
+    while (true) {
+      InstantTimeResponse response;
+      try {
+        response = fetchInstantTimeResponse(checkpointId);
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+        throw new HoodieException("Interrupted while requesting the instant 
time from the coordinator", e);
+      } catch (Exception e) {
+        // transient transport/coordinator error: retry within the budget, 
reusing the same checkpoint identity.
+        if (System.nanoTime() >= deadlineNanos) {
+          throw new HoodieException("Timeout requesting the instant time from 
the coordinator for checkpoint " + checkpointId, e);
+        }
+        backoffMs = sleepAndGrow(backoffMs);
+        continue;
+      }
+      if (response.getStatus() == Status.READY) {
+        return response.getInstant();
+      }
+      // PENDING: keep polling, but never reset the deadline.
+      if (System.nanoTime() >= deadlineNanos) {
+        throw new HoodieException("Timeout waiting for the instant time from 
the coordinator for checkpoint " + checkpointId);
+      }
+      backoffMs = sleepAndGrow(backoffMs);
+    }
+  }
+
+  /**
+   * Sends a single instant-time request to the coordinator and returns its 
response.
+   *
+   * <p>Isolated so tests can stub the transport while reusing the poll loop 
in {@link #requestInstantTime}.
+   */
+  protected InstantTimeResponse fetchInstantTimeResponse(long checkpointId) 
throws Exception {
+    return 
CoordinationResponseSerDe.unwrap(this.gateway.sendRequestToCoordinator(this.operatorID,
+        new 
SerializedValue<>(InstantTimeRequest.getInstance(checkpointId))).get());
+  }
+
+  private static long sleepAndGrow(long backoffMs) {
+    long capped = Math.min(backoffMs, POLL_CAP_MS);

Review Comment:
   This cap appears redundant: `backoffMs` starts below `POLL_CAP_MS`, and 
`sleepAndGrow()` already caps its return value. Could we use `backoffMs` 
directly when calculating the jitter?



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