This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new f3c8f2f887 Fix Join hanging when a parallel branch fails before 
reaching Join (#8082)
f3c8f2f887 is described below

commit f3c8f2f88730468fc340ef4a48530f553c958589
Author: Lance <[email protected]>
AuthorDate: Tue Aug 25 21:43:12 2026 +0800

    Fix Join hanging when a parallel branch fails before reaching Join (#8082)
    
    Signed-off-by: lance <[email protected]>
---
 .../0012-join-failure-with-intermediate.hwf        | 168 +++++++++++++++++
 integration-tests/actions/main-0012-join.hwf       |  33 +++-
 .../hop/workflow/actions/join/ActionJoin.java      | 154 ++++++++++++++--
 .../join/messages/messages_en_US.properties        |   1 +
 .../hop/workflow/actions/join/ActionJoinTest.java  | 203 ++++++++++++++++++++-
 5 files changed, 543 insertions(+), 16 deletions(-)

diff --git a/integration-tests/actions/0012-join-failure-with-intermediate.hwf 
b/integration-tests/actions/0012-join-failure-with-intermediate.hwf
new file mode 100644
index 0000000000..b6a93e2f12
--- /dev/null
+++ b/integration-tests/actions/0012-join-failure-with-intermediate.hwf
@@ -0,0 +1,168 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<workflow>
+  <name>0012-join-failure-with-intermediate</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description/>
+  <extended_description/>
+  <created_user>-</created_user>
+  <modified_user>-</modified_user>
+  <created_date>2026/08/24 00:00:00.000</created_date>
+  <modified_date>2026/08/24 00:00:00.000</modified_date>
+  <workflow_version/>
+  <parameters/>
+  <actions>
+    <action>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <intervalSeconds>0</intervalSeconds>
+      <intervalMinutes>60</intervalMinutes>
+      <DayOfMonth>1</DayOfMonth>
+      <weekDay>1</weekDay>
+      <minutes>0</minutes>
+      <hour>12</hour>
+      <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <xloc>16</xloc>
+      <yloc>80</yloc>
+      <parallel>Y</parallel>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>Join</name>
+      <description/>
+      <type>JOIN</type>
+      <attributes/>
+      <xloc>480</xloc>
+      <yloc>80</yloc>
+      <parallel>N</parallel>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>Success</name>
+      <description/>
+      <type>SUCCESS</type>
+      <attributes/>
+      <xloc>608</xloc>
+      <yloc>80</yloc>
+      <parallel>N</parallel>
+      <attributes_hac/>
+    </action>
+    <action>
+      <maximumTimeout>1</maximumTimeout>
+      <scaletime>0</scaletime>
+      <name>Wait</name>
+      <description/>
+      <type>DELAY</type>
+      <attributes/>
+      <xloc>144</xloc>
+      <yloc>128</yloc>
+      <parallel>N</parallel>
+      <attributes_hac/>
+    </action>
+    <action>
+      <filename>NONE</filename>
+      <name>File exists</name>
+      <description/>
+      <type>FILE_EXISTS</type>
+      <attributes/>
+      <xloc>144</xloc>
+      <yloc>32</yloc>
+      <parallel>N</parallel>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>Dummy after fail</name>
+      <description/>
+      <type>DUMMY</type>
+      <attributes/>
+      <xloc>272</xloc>
+      <yloc>32</yloc>
+      <parallel>N</parallel>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>Dummy after wait</name>
+      <description/>
+      <type>DUMMY</type>
+      <attributes/>
+      <xloc>272</xloc>
+      <yloc>128</yloc>
+      <parallel>N</parallel>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>Wait</to>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Join</from>
+      <to>Success</to>
+      <evaluation>Y</evaluation>
+      <unconditional>N</unconditional>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Start</from>
+      <to>File exists</to>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>File exists</from>
+      <to>Dummy after fail</to>
+      <evaluation>Y</evaluation>
+      <unconditional>N</unconditional>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Dummy after fail</from>
+      <to>Join</to>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Wait</from>
+      <to>Dummy after wait</to>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Dummy after wait</from>
+      <to>Join</to>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+      <enabled>Y</enabled>
+    </hop>
+  </hops>
+  <notepads/>
+  <attributes/>
+</workflow>
diff --git a/integration-tests/actions/main-0012-join.hwf 
b/integration-tests/actions/main-0012-join.hwf
index 010716c6ed..da3ebd77bb 100644
--- a/integration-tests/actions/main-0012-join.hwf
+++ b/integration-tests/actions/main-0012-join.hwf
@@ -80,7 +80,7 @@ limitations under the License.
       <attributes/>
       <parallel>N</parallel>
       <xloc>176</xloc>
-      <yloc>512</yloc>
+      <yloc>624</yloc>
       <attributes_hac/>
     </action>
     <action>
@@ -133,6 +133,30 @@ limitations under the License.
       <yloc>400</yloc>
       <attributes_hac/>
     </action>
+    <action>
+      <name>0012-join-failure-with-intermediate.hwf</name>
+      <description/>
+      <type>WORKFLOW</type>
+      <attributes/>
+      <add_date>N</add_date>
+      <add_time>N</add_time>
+      <create_parent_folder>N</create_parent_folder>
+      <exec_per_row>N</exec_per_row>
+      
<filename>${PROJECT_HOME}/0012-join-failure-with-intermediate.hwf</filename>
+      <loglevel>Nothing</loglevel>
+      <parameters>
+        <pass_all_parameters>Y</pass_all_parameters>
+      </parameters>
+      <params_from_previous>N</params_from_previous>
+      <run_configuration>local</run_configuration>
+      <set_append_logfile>N</set_append_logfile>
+      <set_logfile>N</set_logfile>
+      <wait_until_finished>Y</wait_until_finished>
+      <parallel>N</parallel>
+      <xloc>176</xloc>
+      <yloc>512</yloc>
+      <attributes_hac/>
+    </action>
   </actions>
   <hops>
     <hop>
@@ -158,6 +182,13 @@ limitations under the License.
     </hop>
     <hop>
       <from>0012-join-failure-with-true.hwf</from>
+      <to>0012-join-failure-with-intermediate.hwf</to>
+      <enabled>Y</enabled>
+      <evaluation>N</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+    <hop>
+      <from>0012-join-failure-with-intermediate.hwf</from>
       <to>Success</to>
       <enabled>Y</enabled>
       <evaluation>N</evaluation>
diff --git 
a/plugins/actions/join/src/main/java/org/apache/hop/workflow/actions/join/ActionJoin.java
 
b/plugins/actions/join/src/main/java/org/apache/hop/workflow/actions/join/ActionJoin.java
index 45cbe83cb3..0bf568e67c 100644
--- 
a/plugins/actions/join/src/main/java/org/apache/hop/workflow/actions/join/ActionJoin.java
+++ 
b/plugins/actions/join/src/main/java/org/apache/hop/workflow/actions/join/ActionJoin.java
@@ -17,15 +17,21 @@
 
 package org.apache.hop.workflow.actions.join;
 
+import java.time.Duration;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
+import org.apache.commons.lang3.ThreadUtils;
 import org.apache.hop.core.CheckResult;
 import org.apache.hop.core.ICheckResult;
 import org.apache.hop.core.Result;
 import org.apache.hop.core.annotations.Action;
+import org.apache.hop.core.gui.WorkflowTracker;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.workflow.ActionResult;
 import org.apache.hop.workflow.WorkflowHopMeta;
 import org.apache.hop.workflow.WorkflowMeta;
 import org.apache.hop.workflow.action.ActionBase;
@@ -66,39 +72,46 @@ public class ActionJoin extends ActionBase {
   @Override
   public Result execute(Result result, int nr) {
     try {
-
       // Find previous actions to join
       List<ActionMeta> prevActions = getPreviousAction(this, new 
ArrayList<>(), false);
 
-      var workflowTracker = this.parentWorkflow.getWorkflowTracker();
       while (!parentWorkflow.isStopped()) {
-        Thread.sleep(500L);
+        ThreadUtils.sleep(Duration.ofMillis(500L));
         boolean completed = true;
         boolean success = true;
         int errors = 0;
 
-        // Checks if all previous actions have completed successfully
+        // Checks if all previous actions have completed, or can never run
         for (ActionMeta actionMeta : prevActions) {
-          var tracker = workflowTracker.findWorkflowTracker(actionMeta);
-          if (tracker != null) {
-            Result actionResult = tracker.getActionResult().getResult();
-            if (actionResult == null) {
-              completed = false;
-            } else if (!actionResult.isResult()) {
+          Result actionResult = getFinishedActionResult(actionMeta);
+          if (actionResult != null) {
+            if (!actionResult.isResult()) {
               WorkflowHopMeta hopMeta = findWorkflowHop(actionMeta);
               // If one previous action has failure and the hop is true 
evaluation, repeat failure
               // to the join action
-              if (!hopMeta.isUnconditional() && hopMeta.isEvaluation()) {
+              if (hopMeta != null && !hopMeta.isUnconditional() && 
hopMeta.isEvaluation()) {
                 success = false;
                 errors++;
               }
             }
+          } else if (willNeverExecute(actionMeta, new HashSet<>())) {
+            // Predecessor was skipped because an upstream hop was not 
followed (for example a
+            // failed action with only a success hop toward this branch). Do 
not wait forever.
+            if (isUnreachableBecauseOfFailure(actionMeta, new HashSet<>())) {
+              success = false;
+              errors++;
+            }
+            if (isBasic()) {
+              logBasic(
+                  BaseMessages.getString(
+                      PKG, "ActionJoin.Log.PredecessorUnreachable", 
actionMeta.getName()));
+            }
           } else {
             completed = false;
           }
         }
 
-        // If all previous actions have a result
+        // If all previous actions have a result or can never execute
         if (completed) {
           result.setResult(success);
           result.setNrErrors(errors);
@@ -106,6 +119,10 @@ public class ActionJoin extends ActionBase {
         }
       }
     } catch (Exception e) {
+      if (e instanceof InterruptedException) {
+        Thread.currentThread().interrupt();
+      }
+
       result.setNrErrors(1);
       result.setResult(false);
       logError(BaseMessages.getString(PKG, "ActionJoin.Error.CouldNotExecute") 
+ e);
@@ -170,6 +187,119 @@ public class ActionJoin extends ActionBase {
     return null;
   }
 
+  /**
+   * Result of a finished action, or {@code null} if it has not started or is 
still running. Matches
+   * {@link org.apache.hop.workflow.Workflow} hop following: a tracker with a 
null result is the
+   * "started" marker, not a completed execution.
+   */
+  private Result getFinishedActionResult(ActionMeta actionMeta) {
+    WorkflowTracker<?> tracker =
+        parentWorkflow.getWorkflowTracker().findWorkflowTracker(actionMeta);
+    if (tracker == null) {
+      return null;
+    }
+
+    ActionResult actionResult = tracker.getActionResult();
+    if (actionResult == null) {
+      return null;
+    }
+    return actionResult.getResult();
+  }
+
+  /**
+   * Same condition as {@link org.apache.hop.workflow.Workflow} when deciding 
whether to execute the
+   * next action after {@code fromAction} finished with {@code fromResult}.
+   */
+  private static boolean isHopFollowed(
+      WorkflowHopMeta hop, ActionMeta fromAction, Result fromResult) {
+    return hop.isUnconditional()
+        || (fromAction.isEvaluation() && hop.isEvaluation() == 
fromResult.isResult());
+  }
+
+  /**
+   * True when {@code actionMeta} has not started and every enabled incoming 
hop is dead: the
+   * previous action finished without following the hop, or that previous 
action itself will never
+   * execute. Conservatively returns false if a predecessor is still running 
or is about to start.
+   */
+  private boolean willNeverExecute(ActionMeta actionMeta, Set<ActionMeta> 
visiting) {
+    if (getFinishedActionResult(actionMeta) != null) {
+      return false;
+    }
+    if (parentWorkflow.getWorkflowTracker().findWorkflowTracker(actionMeta) != 
null
+        || actionMeta.isStart()) {
+      return false;
+    }
+    if (!visiting.add(actionMeta)) {
+      return false;
+    }
+
+    List<WorkflowHopMeta> incoming = findIncomingHops(actionMeta);
+    if (incoming.isEmpty()) {
+      return true;
+    }
+
+    for (WorkflowHopMeta hop : incoming) {
+      ActionMeta fromAction = hop.getFromAction();
+      if (fromAction == null) {
+        continue;
+      }
+
+      Result fromResult = getFinishedActionResult(fromAction);
+      if (fromResult != null) {
+        if (isHopFollowed(hop, fromAction, fromResult)) {
+          return false;
+        }
+      } else if (!willNeverExecute(fromAction, visiting)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * True when the action is unreachable because a success hop was not 
followed after a failure.
+   * False when it is unreachable because a failure hop was not followed after 
a success, so Join
+   * should not fail.
+   */
+  private boolean isUnreachableBecauseOfFailure(ActionMeta actionMeta, 
Set<ActionMeta> visiting) {
+    if (!visiting.add(actionMeta)) {
+      return false;
+    }
+    for (WorkflowHopMeta hop : findIncomingHops(actionMeta)) {
+      ActionMeta fromAction = hop.getFromAction();
+      if (fromAction == null) {
+        continue;
+      }
+      Result fromResult = getFinishedActionResult(fromAction);
+      if (fromResult != null) {
+        if (!isHopFollowed(hop, fromAction, fromResult)
+            && !fromResult.isResult()
+            && !hop.isUnconditional()
+            && hop.isEvaluation()) {
+          return true;
+        }
+      } else if (willNeverExecute(fromAction, new HashSet<>())
+          && isUnreachableBecauseOfFailure(fromAction, visiting)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private List<WorkflowHopMeta> findIncomingHops(ActionMeta toAction) {
+    List<WorkflowHopMeta> incoming = new ArrayList<>();
+    if (parentWorkflowMeta == null) {
+      return incoming;
+    }
+
+    for (WorkflowHopMeta hop : parentWorkflowMeta.getWorkflowHops()) {
+      if (hop.isEnabled() && hop.getToAction() != null && 
hop.getToAction().equals(toAction)) {
+        incoming.add(hop);
+      }
+    }
+    return incoming;
+  }
+
   /** Find previous actions */
   private List<ActionMeta> getPreviousAction(
       IAction action, List<ActionMeta> prevActions, boolean deep) {
diff --git 
a/plugins/actions/join/src/main/resources/org/apache/hop/workflow/actions/join/messages/messages_en_US.properties
 
b/plugins/actions/join/src/main/resources/org/apache/hop/workflow/actions/join/messages/messages_en_US.properties
index b73657a13e..f0b3670b89 100644
--- 
a/plugins/actions/join/src/main/resources/org/apache/hop/workflow/actions/join/messages/messages_en_US.properties
+++ 
b/plugins/actions/join/src/main/resources/org/apache/hop/workflow/actions/join/messages/messages_en_US.properties
@@ -21,5 +21,6 @@ ActionJoin.CheckResult.NoParallelExecution=No parallel 
execution to join
 ActionJoin.Description=Join branch of a workflow executed in parallel
 ActionJoin.Error.CouldNotExecute=Couldn't execute join action
 ActionJoin.Keyword=join,synchronize,merge,parallel,branch
+ActionJoin.Log.PredecessorUnreachable=Predecessor action ''{0}'' will never 
execute; Join will not wait for it
 ActionJoin.Name=Join
 ActionJoinDialog.Title=Join
diff --git 
a/plugins/actions/join/src/test/java/org/apache/hop/workflow/actions/join/ActionJoinTest.java
 
b/plugins/actions/join/src/test/java/org/apache/hop/workflow/actions/join/ActionJoinTest.java
index 3f753b2578..d1ccaf6197 100644
--- 
a/plugins/actions/join/src/test/java/org/apache/hop/workflow/actions/join/ActionJoinTest.java
+++ 
b/plugins/actions/join/src/test/java/org/apache/hop/workflow/actions/join/ActionJoinTest.java
@@ -23,19 +23,30 @@ import static 
org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.mock;
 
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.apache.commons.lang3.ThreadUtils;
 import org.apache.hop.core.Result;
+import org.apache.hop.core.logging.LogLevel;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
 import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.workflow.WorkflowHopMeta;
 import org.apache.hop.workflow.WorkflowMeta;
+import org.apache.hop.workflow.action.ActionBase;
+import org.apache.hop.workflow.action.ActionMeta;
+import org.apache.hop.workflow.actions.dummy.ActionDummy;
+import org.apache.hop.workflow.actions.start.ActionStart;
 import org.apache.hop.workflow.engine.IWorkflowEngine;
 import org.apache.hop.workflow.engines.local.LocalWorkflowEngine;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
 import org.junit.jupiter.api.extension.ExtendWith;
 
+/** Unit test for {@link ActionJoin} */
 @ExtendWith(RestoreHopEngineEnvironmentExtension.class)
 class ActionJoinTest {
 
@@ -116,7 +127,7 @@ class ActionJoinTest {
   }
 
   @Test
-  void testExecuteWithNoPreviousActions() throws Exception {
+  void testExecuteWithNoPreviousActions() {
     Result result = new Result();
     Result executionResult = action.execute(result, 0);
 
@@ -125,7 +136,7 @@ class ActionJoinTest {
   }
 
   @Test
-  void testExecuteWithPreviousActions() throws Exception {
+  void testExecuteWithPreviousActions() {
     // Test execute with no previous actions (simplified test)
     Result result = new Result();
     Result executionResult = action.execute(result, 0);
@@ -135,7 +146,7 @@ class ActionJoinTest {
   }
 
   @Test
-  void testExecuteWithException() throws Exception {
+  void testExecuteWithException() {
     // Test execute with exception handling
     Result result = new Result();
     Result executionResult = action.execute(result, 0);
@@ -208,4 +219,190 @@ class ActionJoinTest {
     // The method should execute without errors
     assertNotNull(remarks);
   }
+
+  @Test
+  @Timeout(value = 15, unit = TimeUnit.SECONDS)
+  void executeDoesNotHangWhenPredecessorNeverRunsAfterBranchFailure() {
+    WorkflowMeta meta = new WorkflowMeta();
+    meta.setName("join-unreachable-after-failure");
+
+    ActionMeta startMeta = new ActionMeta(new ActionStart("Start"));
+    startMeta.setLaunchingInParallel(true);
+    ActionMeta successMeta = new ActionMeta(new ActionDummy("Success branch"));
+    ActionMeta failMeta = new ActionMeta(new FailingEvalAction("Fail"));
+    ActionMeta afterFailMeta = new ActionMeta(new ActionDummy("After fail"));
+    ActionMeta joinMeta = new ActionMeta(new ActionJoin("Join", ""));
+
+    meta.addAction(startMeta);
+    meta.addAction(successMeta);
+    meta.addAction(failMeta);
+    meta.addAction(afterFailMeta);
+    meta.addAction(joinMeta);
+
+    meta.addWorkflowHop(new WorkflowHopMeta(startMeta, successMeta));
+    meta.addWorkflowHop(new WorkflowHopMeta(startMeta, failMeta));
+
+    WorkflowHopMeta successToJoin = new WorkflowHopMeta(successMeta, joinMeta);
+    successToJoin.setUnconditional();
+    meta.addWorkflowHop(successToJoin);
+
+    WorkflowHopMeta failToAfter = new WorkflowHopMeta(failMeta, afterFailMeta);
+    failToAfter.setConditional();
+    failToAfter.setEvaluation(true);
+    meta.addWorkflowHop(failToAfter);
+
+    WorkflowHopMeta afterToJoin = new WorkflowHopMeta(afterFailMeta, joinMeta);
+    afterToJoin.setUnconditional();
+    meta.addWorkflowHop(afterToJoin);
+
+    LocalWorkflowEngine engine = new LocalWorkflowEngine(meta);
+    engine.setLogLevel(LogLevel.MINIMAL);
+    Result result = engine.startExecution();
+
+    assertFalse(result.isResult());
+    assertTrue(result.getNrErrors() >= 1);
+  }
+
+  @Test
+  @Timeout(value = 15, unit = TimeUnit.SECONDS)
+  void executeSucceedsWhenPredecessorNeverRunsBecauseFailureHopWasSkipped() {
+    WorkflowMeta meta = new WorkflowMeta();
+    meta.setName("join-unreachable-after-success");
+
+    ActionMeta startMeta = new ActionMeta(new ActionStart("Start"));
+    startMeta.setLaunchingInParallel(true);
+    ActionMeta successMeta = new ActionMeta(new ActionDummy("Success branch"));
+    ActionMeta evalMeta = new ActionMeta(new SucceedingEvalAction("Eval 
success"));
+    ActionMeta neverMeta = new ActionMeta(new ActionDummy("Never run"));
+    ActionMeta joinMeta = new ActionMeta(new ActionJoin("Join", ""));
+
+    meta.addAction(startMeta);
+    meta.addAction(successMeta);
+    meta.addAction(evalMeta);
+    meta.addAction(neverMeta);
+    meta.addAction(joinMeta);
+
+    meta.addWorkflowHop(new WorkflowHopMeta(startMeta, successMeta));
+    meta.addWorkflowHop(new WorkflowHopMeta(startMeta, evalMeta));
+
+    WorkflowHopMeta successToJoin = new WorkflowHopMeta(successMeta, joinMeta);
+    successToJoin.setUnconditional();
+    meta.addWorkflowHop(successToJoin);
+
+    WorkflowHopMeta evalToNever = new WorkflowHopMeta(evalMeta, neverMeta);
+    evalToNever.setConditional();
+    evalToNever.setEvaluation(false);
+    meta.addWorkflowHop(evalToNever);
+
+    WorkflowHopMeta neverToJoin = new WorkflowHopMeta(neverMeta, joinMeta);
+    neverToJoin.setUnconditional();
+    meta.addWorkflowHop(neverToJoin);
+
+    LocalWorkflowEngine engine = new LocalWorkflowEngine(meta);
+    engine.setLogLevel(LogLevel.MINIMAL);
+    Result result = engine.startExecution();
+
+    assertTrue(result.isResult());
+    assertEquals(0, result.getNrErrors());
+  }
+
+  @Test
+  @Timeout(value = 15, unit = TimeUnit.SECONDS)
+  void executeStillWaitsForSlowPredecessorThatHasNotStartedYet() {
+    WorkflowMeta meta = new WorkflowMeta();
+    meta.setName("join-wait-for-slow-branch");
+
+    ActionMeta startMeta = new ActionMeta(new ActionStart("Start"));
+    startMeta.setLaunchingInParallel(true);
+    ActionMeta fastMeta = new ActionMeta(new ActionDummy("Fast branch"));
+    ActionMeta slowMeta = new ActionMeta(new SleepingEvalAction("Slow branch", 
800));
+    ActionMeta joinMeta = new ActionMeta(new ActionJoin("Join", ""));
+
+    meta.addAction(startMeta);
+    meta.addAction(fastMeta);
+    meta.addAction(slowMeta);
+    meta.addAction(joinMeta);
+
+    meta.addWorkflowHop(new WorkflowHopMeta(startMeta, fastMeta));
+    meta.addWorkflowHop(new WorkflowHopMeta(startMeta, slowMeta));
+
+    WorkflowHopMeta fastToJoin = new WorkflowHopMeta(fastMeta, joinMeta);
+    fastToJoin.setUnconditional();
+    meta.addWorkflowHop(fastToJoin);
+
+    WorkflowHopMeta slowToJoin = new WorkflowHopMeta(slowMeta, joinMeta);
+    slowToJoin.setUnconditional();
+    meta.addWorkflowHop(slowToJoin);
+
+    LocalWorkflowEngine engine = new LocalWorkflowEngine(meta);
+    engine.setLogLevel(LogLevel.MINIMAL);
+    Result result = engine.startExecution();
+
+    assertTrue(result.isResult());
+    
assertNotNull(engine.getWorkflowTracker().findWorkflowTracker(slowMeta).getActionResult());
+    assertNotNull(
+        
engine.getWorkflowTracker().findWorkflowTracker(slowMeta).getActionResult().getResult());
+  }
+
+  static class FailingEvalAction extends ActionBase {
+    FailingEvalAction(String name) {
+      super(name, "");
+    }
+
+    @Override
+    public Result execute(Result result, int nr) {
+      result.setResult(false);
+      result.setNrErrors(1);
+      return result;
+    }
+
+    @Override
+    public boolean isEvaluation() {
+      return true;
+    }
+  }
+
+  static class SucceedingEvalAction extends ActionBase {
+    SucceedingEvalAction(String name) {
+      super(name, "");
+    }
+
+    @Override
+    public Result execute(Result result, int nr) {
+      result.setResult(true);
+      result.setNrErrors(0);
+      return result;
+    }
+
+    @Override
+    public boolean isEvaluation() {
+      return true;
+    }
+  }
+
+  static class SleepingEvalAction extends ActionBase {
+    private final long sleepMs;
+
+    SleepingEvalAction(String name, long sleepMs) {
+      super(name, "");
+      this.sleepMs = sleepMs;
+    }
+
+    @Override
+    public Result execute(Result result, int nr) {
+      try {
+        ThreadUtils.sleep(Duration.ofMillis(sleepMs));
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+      }
+      result.setResult(true);
+      result.setNrErrors(0);
+      return result;
+    }
+
+    @Override
+    public boolean isEvaluation() {
+      return true;
+    }
+  }
 }

Reply via email to