mattcasters commented on code in PR #9035:
URL: https://github.com/apache/hop/pull/9035#discussion_r4334089355


##########
engine/src/main/java/org/apache/hop/pipeline/Pipeline.java:
##########
@@ -1579,15 +1610,21 @@ public void fireExecutionFinishedListeners() throws 
HopException {
     }
 
     try {
-      // Now the status and everything else is set correctly. We've completed 
the pipeline.
-      //
-      pipelineCompleted();
-
-      // Also call an extension point in case plugins want to play along
+      // Call the completion extension point before releasing anything blocked 
in
+      // waitUntilFinished(): plugins here may still have work to do - 
flushing lineage, for
+      // example - while a waiting caller (hop-run, for one) can leave the JVM 
the moment it is
+      // released.
       //
       ExtensionPointHandler.callExtensionPoint(
           log, this, HopExtensionPoint.PipelineCompleted.id, this);
     } finally {
+      // Now the status and everything else is set correctly. We've completed 
the pipeline.
+      //
+      // Always release the waiters, even when an extension point failed: that 
must never be able to
+      // deadlock a caller. The extension point failure itself still 
propagates.
+      //
+      pipelineCompleted();

Review Comment:
   **[suggestion]** Releasing the waiter here happens before a 
`PipelineCompleted` failure is visible on the result. The exception leaves 
`fireExecutionFinishedListeners()` only after this `finally`, including 
`releaseVfsNamespace()`, and the transform-finished listener increments the 
transform error count only after that. `HopRun` reads 
`getResult().getNrErrors()` as soon as `waitUntilFinished()` returns, so a 
throwing extension point can still produce exit code 0. `ExtensionPointMap` 
does not wrap runtime exceptions, so those take the same path.
   
   **Suggestion:** Record the failure on the pipeline `errors` count before 
`pipelineCompleted()`. `getResult()` already adds that field. Drop the extra 
`setErrors` in the transform listener for this same failure, or the run is 
counted twice. Catch `Exception`, not only `HopException`.



##########
engine/src/test/java/org/apache/hop/pipeline/PipelineTest.java:
##########
@@ -150,6 +163,182 @@ void 
testFireExecutionFinishedListenersExceptionOnPipelineFinished() throws Exce
     assertThrows(HopException.class, line::fireExecutionFinishedListeners);
   }
 
+  /**
+   * A caller blocked in {@link Pipeline#waitUntilFinished()} must not be 
released before the
+   * PipelineCompleted extension point has run. Extension points there still 
have work to flush -
+   * lineage for one - and a released caller (hop-run, for example) can leave 
the JVM immediately.
+   */
+  @Test
+  void pipelineCompletedExtensionPointRunsBeforeWaitersAreReleased() throws 
Exception {
+    List<String> order = Collections.synchronizedList(new ArrayList<>());
+    CountDownLatch waiterReleased = new CountDownLatch(1);
+
+    TestExtensionPointPlugin plugin =
+        registerExtensionPoint(
+            HopExtensionPoint.PipelineCompleted.id,
+            () -> {
+              order.add("extension-point");
+              // If the waiter was released first (the bug) it records itself 
while this runs.
+              // Waiting for that here keeps the assertion deterministic 
instead of a race.
+              try {
+                waiterReleased.await(400, TimeUnit.MILLISECONDS);
+              } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+              }
+            });
+
+    try {
+      Pipeline line = new LocalPipelineEngine(new PipelineMeta());
+      line.setLogChannel(mock(ILogChannel.class));
+      line.prepareExecution();
+
+      Thread waiter =
+          new Thread(
+              () -> {
+                line.waitUntilFinished();

Review Comment:
   **[bug]** This waiter calls `waitUntilFinished()` before `startThreads()` 
creates `pipelineWaitUntilFinishedBlockingQueue`. That method returns 
immediately while the queue is still null, so the waiter appends `"waiter"` and 
counts the latch down before `PipelineCompleted` runs. `startThreads()` invokes 
`PipelineStartThreads` and other extension points before allocating the queue, 
so the new thread often wins that window and the assertion sees `[waiter, 
extension-point]` even though the production order is correct. The 400ms wait 
inside the extension point does not cover this case: the waiter has already 
recorded itself.
   
   **Suggestion:** Do not call `waitUntilFinished()` until the queue exists. It 
is assigned before `setRunning(true)`, and an empty pipeline signals only after 
that. Spin while `!line.isRunning() && !line.isFinished()`, then wait. A late 
waiter still observes the object already on the queue, so the expected order 
stays deterministic without depending on the null-queue return.



##########
engine/src/main/java/org/apache/hop/run/HopRun.java:
##########
@@ -92,6 +93,14 @@ public static void main(String[] args) {
         System.exit(1);
       } else {
         hopRun.run();
+
+        // Deliver whatever lineage is still queued before the JVM goes away: 
the hub hands its
+        // queue to the sinks and the sinks drain theirs (bounded by their 
drain timeout). Without
+        // it a short run loses its terminal events, which the sinks send 
last. Safe to call when
+        // lineage is disabled.
+        //
+        LineageHub.getInstance().shutdown();

Review Comment:
   **[suggestion]** `LineageHub.shutdown()` runs only when `hopRun.run()` 
returns. The `ExecutionException` and other catch paths call `System.exit` 
without draining. `TransformBeforeStart` can now throw from `startThreads()` 
after an earlier transform thread has already emitted, and the OpenLineage sink 
drains its own dispatcher only from `shutdown()`.
   
   **Suggestion:** Call `shutdown()` on those failure paths immediately before 
`System.exit` as well. A `finally` attached to this try does not run once 
`System.exit` has been entered from a catch. The call is already a no-op when 
the hub never started.



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