hudi-agent commented on code in PR #19867:
URL: https://github.com/apache/hudi/pull/19867#discussion_r3956292898


##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/heartbeat/HoodieHeartbeatClient.java:
##########
@@ -196,15 +196,34 @@ private boolean isHeartbeatStarted(Heartbeat heartbeat) {
   private void stopHeartbeatScheduler(Heartbeat heartbeat) {
     log.info("Stopping heartbeat for instant {}", heartbeat.getInstantTime());
     shutdownHeartbeatScheduler(heartbeat);
+    // Callers delete the heartbeat file next. A refresh landing after that 
delete recreates the
+    // file, and on storage that enforces preconditions it also changes the 
object generation, so a
+    // generation-matched delete is rejected (e.g. GCS 412 conditionNotMet).
+    awaitHeartbeatSchedulerTermination(heartbeat);
     heartbeat.setHeartbeatStopped(true);
     log.info("Stopped heartbeat for instant {}", heartbeat.getInstantTime());
   }
 
+  /** Stops further refreshes without waiting; safe to call from the scheduler 
thread itself. */
   private void shutdownHeartbeatScheduler(Heartbeat heartbeat) {
     if (heartbeat.getScheduledFuture() != null) {
       heartbeat.getScheduledFuture().cancel(false);
     }
-    heartbeat.getHeartbeatScheduler().shutdownNow();
+    heartbeat.getHeartbeatScheduler().shutdown();
+  }
+
+  private void awaitHeartbeatSchedulerTermination(Heartbeat heartbeat) {
+    // An in-flight tick can be parked on the bounded write, so allow for that 
plus one interval.
+    long timeoutMs = this.heartbeatWriteTimeoutMs + this.heartbeatIntervalInMs;
+    try {
+      if (!heartbeat.getHeartbeatScheduler().awaitTermination(timeoutMs, 
TimeUnit.MILLISECONDS)) {

Review Comment:
   πŸ€– `close()` is `synchronized` and so is `getHeartbeatWriteExecutor()`, both 
on `this`. If a tick has just entered `updateHeartbeat` when `close()` starts, 
it blocks on the monitor while `close()` sits here in `awaitTermination` 
holding it β€” so `close()` stalls for the full timeout (2Γ—interval, ~120s at 
defaults), and after it nulls `heartbeatWriteExecutor` the tick lazily creates 
a fresh pool. Could the await be done outside the monitor, or the executor 
guarded by a separate lock / created eagerly?
   
   <sub><i>⚠️ AI-generated; verify before applying. React πŸ‘/πŸ‘Ž to flag 
quality.</i></sub>



##########
hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/heartbeat/HoodieHeartbeatClient.java:
##########
@@ -196,15 +196,34 @@ private boolean isHeartbeatStarted(Heartbeat heartbeat) {
   private void stopHeartbeatScheduler(Heartbeat heartbeat) {
     log.info("Stopping heartbeat for instant {}", heartbeat.getInstantTime());
     shutdownHeartbeatScheduler(heartbeat);
+    // Callers delete the heartbeat file next. A refresh landing after that 
delete recreates the
+    // file, and on storage that enforces preconditions it also changes the 
object generation, so a
+    // generation-matched delete is rejected (e.g. GCS 412 conditionNotMet).
+    awaitHeartbeatSchedulerTermination(heartbeat);

Review Comment:
   πŸ€– When the bounded write itself times out, `future.cancel(true)` interrupts 
the write thread but a storage upload already on the wire may still land; the 
scheduler thread returns, this await succeeds, and the delete runs before that 
late create. Is that residual window intended to be covered only by the new 
`HoodieIOException` catch (leaving an orphan heartbeat file), or would it be 
worth also awaiting the outstanding write future here?
   
   <sub><i>⚠️ AI-generated; verify before applying. React πŸ‘/πŸ‘Ž to flag 
quality.</i></sub>



##########
hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/heartbeat/TestHoodieHeartbeatClient.java:
##########
@@ -165,6 +166,91 @@ public void 
testScheduledHeartbeatRetriesAfterWriteFailure() {
     }
   }
 
+  /**
+   * stop() must not delete the heartbeat file while a scheduled refresh is 
still writing it. A
+   * refresh landing after the delete recreates the file, and on storage that 
enforces preconditions
+   * it changes the object generation so a generation-matched delete is 
rejected (e.g. GCS 412).
+   */
+  @Test
+  public void testStopWaitsForInFlightHeartbeatRefresh() throws Exception {
+    CountDownLatch refreshEntered = new CountDownLatch(1);
+    CountDownLatch releaseRefresh = new CountDownLatch(1);
+    OrderRecordingStorage storage = new OrderRecordingStorage(
+        (FileSystem) metaClient.getStorage().getFileSystem(), refreshEntered, 
releaseRefresh);
+    HoodieHeartbeatClient client = new HoodieHeartbeatClient(
+        storage, metaClient.getBasePath().toString(), heartBeatInterval, 
numTolerableMisses);

Review Comment:
   πŸ€– With `heartBeatInterval = 1000L` the write timeout is also 1s. If thread 
start + Awaitility's default 100ms poll delay/interval push the gap between 
`refreshEntered` and `releaseRefresh.countDown()` past ~1s on a loaded CI box, 
`future.get` times out, `awaitTermination` returns immediately, the delete runs 
before the gated create lands, and the last-event assertion fails spuriously. 
Would a larger interval for this test (e.g. 5000L like 
`testNumHeartbeatsGenerated`) be safer?
   
   <sub><i>⚠️ AI-generated; verify before applying. React πŸ‘/πŸ‘Ž to flag 
quality.</i></sub>



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