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 e836fc9fa0 status on transform can change mid check, fixes #9096 
(#9109)
e836fc9fa0 is described below

commit e836fc9fa05d6b8cd50e325b3b910048840dc25c
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Fri Oct 30 10:23:41 2026 +0100

    status on transform can change mid check, fixes #9096 (#9109)
---
 .../BlockUntilTransformsFinish.java                |  21 ++--
 .../BlockUntilTransformsFinishTest.java            | 116 +++++++++++++++++++++
 2 files changed, 126 insertions(+), 11 deletions(-)

diff --git 
a/plugins/transforms/blockuntiltransformsfinish/src/main/java/org/apache/hop/pipeline/transforms/blockuntiltransformsfinish/BlockUntilTransformsFinish.java
 
b/plugins/transforms/blockuntiltransformsfinish/src/main/java/org/apache/hop/pipeline/transforms/blockuntiltransformsfinish/BlockUntilTransformsFinish.java
index ac37ae185a..2a99a11f82 100644
--- 
a/plugins/transforms/blockuntiltransformsfinish/src/main/java/org/apache/hop/pipeline/transforms/blockuntiltransformsfinish/BlockUntilTransformsFinish.java
+++ 
b/plugins/transforms/blockuntiltransformsfinish/src/main/java/org/apache/hop/pipeline/transforms/blockuntiltransformsfinish/BlockUntilTransformsFinish.java
@@ -24,7 +24,7 @@ import org.apache.hop.core.exception.HopException;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.pipeline.Pipeline;
 import org.apache.hop.pipeline.PipelineMeta;
-import org.apache.hop.pipeline.engine.EngineComponent;
+import org.apache.hop.pipeline.engine.EngineComponent.ComponentExecutionStatus;
 import org.apache.hop.pipeline.engine.IEngineComponent;
 import org.apache.hop.pipeline.transform.BaseTransform;
 import org.apache.hop.pipeline.transform.TransformMeta;
@@ -100,23 +100,22 @@ public class BlockUntilTransformsFinish
       while (it.hasNext()) {
         Entry<Integer, IEngineComponent> e = it.next();
         IEngineComponent transform = e.getValue();
-        if (transform.getStatus() == 
EngineComponent.ComponentExecutionStatus.STATUS_RUNNING
-            || transform.getStatus() == 
EngineComponent.ComponentExecutionStatus.STATUS_IDLE
-            || transform.getStatus() == 
EngineComponent.ComponentExecutionStatus.STATUS_INIT
-            || transform.getStatus() == 
EngineComponent.ComponentExecutionStatus.STATUS_PAUSED) {
+        // Read the status once: it can change between reads, e.g. from idle 
to running at
+        // startup, and then none of the checks below would match (#9096).
+        ComponentExecutionStatus status = transform.getStatus();
+        if (status == ComponentExecutionStatus.STATUS_RUNNING
+            || status == ComponentExecutionStatus.STATUS_IDLE
+            || status == ComponentExecutionStatus.STATUS_INIT
+            || status == ComponentExecutionStatus.STATUS_PAUSED) {
           // This transform is still running...
           data.continueLoop = true;
         } else {
           if (isBasic()) {
-            logBasic(
-                "Transform "
-                    + transform.getName()
-                    + " status: "
-                    + transform.getStatusDescription());
+            logBasic("Transform " + transform.getName() + " status: " + 
status.getDescription());
           }
           // We have done with this transform.
           // remove it from the map
-          data.componentMap.remove(e.getKey());
+          it.remove();
           if (isDetailed()) {
             logDetailed(
                 "Finished running transform ["
diff --git 
a/plugins/transforms/blockuntiltransformsfinish/src/test/java/org/apache/hop/pipeline/transforms/blockuntiltransformsfinish/BlockUntilTransformsFinishTest.java
 
b/plugins/transforms/blockuntiltransformsfinish/src/test/java/org/apache/hop/pipeline/transforms/blockuntiltransformsfinish/BlockUntilTransformsFinishTest.java
new file mode 100644
index 0000000000..6b7872a492
--- /dev/null
+++ 
b/plugins/transforms/blockuntiltransformsfinish/src/test/java/org/apache/hop/pipeline/transforms/blockuntiltransformsfinish/BlockUntilTransformsFinishTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.
+ */
+
+package org.apache.hop.pipeline.transforms.blockuntiltransformsfinish;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.hop.core.logging.ILoggingObject;
+import org.apache.hop.pipeline.engine.EngineComponent.ComponentExecutionStatus;
+import org.apache.hop.pipeline.engine.IEngineComponent;
+import org.apache.hop.pipeline.transforms.mock.TransformMockHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class BlockUntilTransformsFinishTest {
+  private static final String WAITED_FOR = "write to table";
+
+  private TransformMockHelper<BlockUntilTransformsFinishMeta, 
BlockUntilTransformsFinishData>
+      helper;
+
+  @BeforeEach
+  void setup() {
+    helper =
+        new TransformMockHelper<>(
+            "BLOCK UNTIL TRANSFORMS FINISH TEST",
+            BlockUntilTransformsFinishMeta.class,
+            BlockUntilTransformsFinishData.class);
+    when(helper.logChannelFactory.create(any(), any(ILoggingObject.class)))
+        .thenReturn(helper.iLogChannel);
+  }
+
+  @AfterEach
+  void tearDown() {
+    helper.cleanUp();
+  }
+
+  /**
+   * #9096: the waited-for transform switches from idle to running while its 
status is checked. The
+   * first status read sees IDLE and every later read sees RUNNING, so a check 
that reads the status
+   * more than once matches none of the "still busy" states and stops waiting.
+   */
+  @Test
+  void waitsWhenTransformStartsRunningDuringStatusCheck() throws Exception {
+    AtomicInteger statusReads = new AtomicInteger();
+    AtomicBoolean finished = new AtomicBoolean();
+    IEngineComponent waitedFor = mock(IEngineComponent.class);
+    when(waitedFor.getName()).thenReturn(WAITED_FOR);
+    when(waitedFor.getStatus())
+        .thenAnswer(
+            invocation -> {
+              int read = statusReads.incrementAndGet();
+              if (read == 1) {
+                return ComponentExecutionStatus.STATUS_IDLE;
+              }
+              if (read < 10) {
+                return ComponentExecutionStatus.STATUS_RUNNING;
+              }
+              finished.set(true);
+              return ComponentExecutionStatus.STATUS_FINISHED;
+            });
+    when(helper.pipeline.findComponent(WAITED_FOR, 0)).thenReturn(waitedFor);
+
+    BlockUntilTransformsFinishMeta meta = new BlockUntilTransformsFinishMeta();
+    meta.setBlockingTransforms(List.of(new BlockingTransform(WAITED_FOR, 
"0")));
+
+    BlockUntilTransformsFinish transform =
+        spy(
+            new BlockUntilTransformsFinish(
+                helper.transformMeta,
+                meta,
+                new BlockUntilTransformsFinishData(),
+                0,
+                helper.pipelineMeta,
+                helper.pipeline));
+
+    AtomicBoolean rowPassedBeforeFinish = new AtomicBoolean();
+    doReturn(new Object[] {1L}).when(transform).getRow();
+    doAnswer(
+            invocation -> {
+              rowPassedBeforeFinish.set(!finished.get());
+              return null;
+            })
+        .when(transform)
+        .putRow(any(), any());
+
+    transform.processRow();
+
+    assertFalse(rowPassedBeforeFinish.get(), "The row was passed on before the 
transform finished");
+    assertTrue(finished.get());
+  }
+}

Reply via email to