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


##########
hudi-utilities/src/test/java/org/apache/hudi/utilities/deltastreamer/TestHoodieDeltaStreamer.java:
##########
@@ -1744,22 +1749,100 @@ static void 
deltaStreamerTestRunner(HoodieDeltaStreamer ds, HoodieDeltaStreamer.
 
   static void deltaStreamerTestRunner(HoodieDeltaStreamer ds, 
HoodieDeltaStreamer.Config cfg, Function<Boolean, Boolean> condition, String 
jobId) throws Exception {
     ExecutorService executor = Executors.newSingleThreadExecutor();
-    Future dsFuture = executor.submit(() -> {
+    Future dsFuture = null;
+    boolean stoppedCleanly = false;
+    try {
+      dsFuture = executor.submit(() -> {
+        try {
+          ds.sync();
+        } catch (Exception ex) {
+          log.warn("DS continuous job failed, hence not proceeding with 
condition check for {}", jobId);
+          throw new RuntimeException(ex.getMessage(), ex);
+        }
+      });
+      TestHelpers.waitTillCondition(condition, dsFuture, 360);
+      if (cfg != null && !cfg.postWriteTerminationStrategyClass.isEmpty()) {
+        // If the streamer died, waitTillCondition returns as soon as the 
future completes. Surface that
+        // failure here rather than letting awaitDeltaStreamerShutdown time 
out and report the misleading
+        // "Deltastreamer should have shutdown by now" two minutes later.
+        if (dsFuture.isDone()) {
+          dsFuture.get();
+        }
+        awaitDeltaStreamerShutdown(ds);
+      } else {
+        ds.shutdownGracefully();
+        dsFuture.get();
+      }
+      stoppedCleanly = true;
+    } finally {
+      if (!stoppedCleanly) {
+        try {
+          stopLeakedStreamer(ds, dsFuture);
+        } catch (Throwable cleanupFailure) {
+          // Never let the cleanup replace the failure the caller is already 
propagating.
+          log.warn("Failed to stop the streamer after a failure", 
cleanupFailure);
+        }
+      }
+      executor.shutdown();
+    }
+  }
+
+  /**
+   * Stops a streamer that a failure left running, without letting the stop 
hang the test.
+   * <p>
+   * Surefire runs this module with forkCount=1 and reuseForks=true, so a live 
streamer reads on into the
+   * next test, whose setup deletes basePath and whose teardown closes the 
data generators underneath it.
+   * The stop has to be bounded: shutdownGracefully awaits the ingest executor 
for up to 24 hours, and it
+   * returns immediately without waiting when shutdown was already requested, 
so neither the wait nor the
+   * absence of one can be relied on here.
+   */
+  private static void stopLeakedStreamer(HoodieDeltaStreamer ds, Future 
dsFuture) {
+    ExecutorService stopper = Executors.newSingleThreadExecutor();
+    try {
+      Future<?> stop = stopper.submit(ds::shutdownGracefully);
       try {
-        ds.sync();
-      } catch (Exception ex) {
-        log.warn("DS continuous job failed, hence not proceeding with 
condition check for {}", jobId);
-        throw new RuntimeException(ex.getMessage(), ex);
+        stop.get(STREAMER_STOP_TIMEOUT_SECS, TimeUnit.SECONDS);
+      } catch (ExecutionException stopThrew) {
+        // The stop itself failing does not excuse leaving the ingest task 
running, so fall through to the join
+        // below rather than take the outer clause, which tolerates only the 
ingest task's own failure.
+        log.warn("Stopping the streamer threw after a failure", stopThrew);
       }
-    });
-    TestHelpers.waitTillCondition(condition, dsFuture, 360);
-    if (cfg != null && !cfg.postWriteTerminationStrategyClass.isEmpty()) {
-      awaitDeltaStreamerShutdown(ds);
-    } else {
-      ds.shutdownGracefully();
-      dsFuture.get();
+      if (dsFuture != null) {
+        dsFuture.get(STREAMER_STOP_TIMEOUT_SECS, TimeUnit.SECONDS);
+      }
+    } catch (ExecutionException ingestFailure) {
+      // Expected rather than anomalous: the ingest task failing is usually 
why the caller is unwinding at
+      // all, and the caller reports it. Nothing to warn about here.
+    } catch (Exception stopFailure) {
+      // Swallowed on purpose: this runs while another failure is propagating, 
and replacing that failure
+      // with this one would hide the diagnostic the caller is about to report.
+      if (stopFailure instanceof InterruptedException) {
+        Thread.currentThread().interrupt();
+      }
+      log.warn("Could not stop the streamer cleanly after a failure, 
cancelling the ingest task", stopFailure);
+      // The 60s bound only stops this thread waiting: 
HoodieAsyncService.shutdown(false) swallows the interrupt
+      // that stopper.shutdownNow() sends, and 
HoodieStreamer.shutdownGracefully runs ds.close() regardless, so
+      // without forcing the executor down the write client can close under a 
still-running ingest round.

Review Comment:
   Done in `aca668b`, softened to "forcing the executor down at least 
interrupts the ingest round before the close". The preceding two lines still 
carry the mechanism, so only the claim at the end changed.
   



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