feat: M28 支持工作流多入边汇聚模式
This commit is contained in:
@@ -634,7 +634,8 @@ public class Chain {
|
|||||||
NodeStateField
|
NodeStateField
|
||||||
.EXECUTION_ATTEMPT_KEY);
|
.EXECUTION_ATTEMPT_KEY);
|
||||||
}
|
}
|
||||||
if (node.getCondition() == null) {
|
if (node.getJoinMode() == NodeJoinMode.ANY
|
||||||
|
&& node.getCondition() == null) {
|
||||||
s.recordTrigger(triggerEdgeId);
|
s.recordTrigger(triggerEdgeId);
|
||||||
fields.add(NodeStateField.TRIGGER_COUNT);
|
fields.add(NodeStateField.TRIGGER_COUNT);
|
||||||
fields.add(NodeStateField.TRIGGER_EDGE_IDS);
|
fields.add(NodeStateField.TRIGGER_EDGE_IDS);
|
||||||
@@ -795,7 +796,7 @@ public class Chain {
|
|||||||
|
|
||||||
private boolean shouldSkipNode(Node node, String edgeId) {
|
private boolean shouldSkipNode(Node node, String edgeId) {
|
||||||
NodeCondition condition = node.getCondition();
|
NodeCondition condition = node.getCondition();
|
||||||
if (condition == null) {
|
if (node.getJoinMode() == NodeJoinMode.ANY && condition == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return executeWithLock(stateInstanceId, 10, TimeUnit.SECONDS, () -> {
|
return executeWithLock(stateInstanceId, 10, TimeUnit.SECONDS, () -> {
|
||||||
@@ -805,7 +806,11 @@ public class Chain {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Map<String, Object> prevResult = Collections.emptyMap();
|
Map<String, Object> prevResult = Collections.emptyMap();
|
||||||
boolean shouldSkipNode = !condition.check(this, newState, prevResult);
|
boolean joinPending = node.getJoinMode() == NodeJoinMode.ALL
|
||||||
|
&& !newState.isUpstreamFullyExecuted();
|
||||||
|
boolean shouldSkipNode = joinPending
|
||||||
|
|| (condition != null
|
||||||
|
&& !condition.check(this, newState, prevResult));
|
||||||
if (shouldSkipNode) {
|
if (shouldSkipNode) {
|
||||||
updateStateSafely(state -> {
|
updateStateSafely(state -> {
|
||||||
return state.addUncheckedNodeId(node.id)
|
return state.addUncheckedNodeId(node.id)
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ public abstract class Node implements Serializable {
|
|||||||
|
|
||||||
protected NodeCondition condition;
|
protected NodeCondition condition;
|
||||||
protected NodeValidator validator;
|
protected NodeValidator validator;
|
||||||
|
protected NodeJoinMode joinMode = NodeJoinMode.ANY;
|
||||||
|
|
||||||
// 循环执行相关属性
|
// 循环执行相关属性
|
||||||
protected boolean loopEnable = false; // 是否启用循环执行
|
protected boolean loopEnable = false; // 是否启用循环执行
|
||||||
@@ -70,6 +71,10 @@ public abstract class Node implements Serializable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setParentId(String parentId) {
|
public void setParentId(String parentId) {
|
||||||
|
if (StringUtil.hasText(parentId) && getJoinMode() == NodeJoinMode.ALL) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"joinMode 'all' is not supported for loop child nodes");
|
||||||
|
}
|
||||||
this.parentId = parentId;
|
this.parentId = parentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +126,31 @@ public abstract class Node implements Serializable {
|
|||||||
this.validator = validator;
|
this.validator = validator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取节点的直接入边汇聚模式。
|
||||||
|
*
|
||||||
|
* <p>旧序列化对象缺少该字段时返回 {@link NodeJoinMode#ANY}。</p>
|
||||||
|
*
|
||||||
|
* @return 汇聚模式
|
||||||
|
*/
|
||||||
|
public NodeJoinMode getJoinMode() {
|
||||||
|
return joinMode == null ? NodeJoinMode.ANY : joinMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置节点的直接入边汇聚模式。
|
||||||
|
*
|
||||||
|
* @param joinMode 汇聚模式
|
||||||
|
*/
|
||||||
|
public void setJoinMode(NodeJoinMode joinMode) {
|
||||||
|
NodeJoinMode resolved = joinMode == null ? NodeJoinMode.ANY : joinMode;
|
||||||
|
if (resolved == NodeJoinMode.ALL && StringUtil.hasText(parentId)) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"joinMode 'all' is not supported for loop child nodes");
|
||||||
|
}
|
||||||
|
this.joinMode = resolved;
|
||||||
|
}
|
||||||
|
|
||||||
// protected void addOutwardEdge(Edge edge) {
|
// protected void addOutwardEdge(Edge edge) {
|
||||||
// if (this.outwardEdges == null) {
|
// if (this.outwardEdges == null) {
|
||||||
// this.outwardEdges = new ArrayList<>();
|
// this.outwardEdges = new ArrayList<>();
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||||
|
* <p>
|
||||||
|
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* <p>
|
||||||
|
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
* <p>
|
||||||
|
* 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 com.easyagents.flow.core.chain;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多入边节点的触发汇聚模式。
|
||||||
|
*/
|
||||||
|
public enum NodeJoinMode {
|
||||||
|
|
||||||
|
/** 任意一条直接入边到达即可执行。 */
|
||||||
|
ANY("any"),
|
||||||
|
/** 全部直接入边到达后才执行。 */
|
||||||
|
ALL("all");
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
NodeJoinMode(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按工作流 JSON 值解析汇聚模式。
|
||||||
|
*
|
||||||
|
* @param value 配置值
|
||||||
|
* @return 汇聚模式
|
||||||
|
* @throws IllegalArgumentException 配置为空或不受支持
|
||||||
|
*/
|
||||||
|
public static NodeJoinMode ofValue(String value) {
|
||||||
|
if (value == null || value.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("joinMode must be 'any' or 'all'");
|
||||||
|
}
|
||||||
|
String normalized = value.trim().toLowerCase(Locale.ROOT);
|
||||||
|
for (NodeJoinMode mode : values()) {
|
||||||
|
if (mode.value.equals(normalized)) {
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Unsupported joinMode: " + value + "; expected 'any' or 'all'");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import com.alibaba.fastjson.JSONObject;
|
|||||||
import com.easyagents.flow.core.chain.DataType;
|
import com.easyagents.flow.core.chain.DataType;
|
||||||
import com.easyagents.flow.core.chain.JsCodeCondition;
|
import com.easyagents.flow.core.chain.JsCodeCondition;
|
||||||
import com.easyagents.flow.core.chain.Node;
|
import com.easyagents.flow.core.chain.Node;
|
||||||
|
import com.easyagents.flow.core.chain.NodeJoinMode;
|
||||||
import com.easyagents.flow.core.chain.Parameter;
|
import com.easyagents.flow.core.chain.Parameter;
|
||||||
import com.easyagents.flow.core.chain.RefType;
|
import com.easyagents.flow.core.chain.RefType;
|
||||||
import com.easyagents.flow.core.node.BaseNode;
|
import com.easyagents.flow.core.node.BaseNode;
|
||||||
@@ -123,6 +124,10 @@ public abstract class BaseNodeParser<T extends BaseNode> implements NodeParser<T
|
|||||||
|
|
||||||
if (!data.isEmpty()) {
|
if (!data.isEmpty()) {
|
||||||
|
|
||||||
|
if (data.containsKey("joinMode")) {
|
||||||
|
node.setJoinMode(NodeJoinMode.ofValue(data.getString("joinMode")));
|
||||||
|
}
|
||||||
|
|
||||||
addParameters(node, data);
|
addParameters(node, data);
|
||||||
addOutputDefs(node, data);
|
addOutputDefs(node, data);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,407 @@
|
|||||||
|
package com.easyagents.flow.core.test;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import com.easyagents.flow.core.chain.Chain;
|
||||||
|
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||||
|
import com.easyagents.flow.core.chain.ChainState;
|
||||||
|
import com.easyagents.flow.core.chain.ChainStatus;
|
||||||
|
import com.easyagents.flow.core.chain.Edge;
|
||||||
|
import com.easyagents.flow.core.chain.Node;
|
||||||
|
import com.easyagents.flow.core.chain.NodeCondition;
|
||||||
|
import com.easyagents.flow.core.chain.NodeJoinMode;
|
||||||
|
import com.easyagents.flow.core.chain.NodeState;
|
||||||
|
import com.easyagents.flow.core.chain.Parameter;
|
||||||
|
import com.easyagents.flow.core.chain.RefType;
|
||||||
|
import com.easyagents.flow.core.chain.repository.InMemoryChainStateRepository;
|
||||||
|
import com.easyagents.flow.core.chain.repository.InMemoryNodeStateRepository;
|
||||||
|
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||||
|
import com.easyagents.flow.core.chain.runtime.InMemoryTriggerStore;
|
||||||
|
import com.easyagents.flow.core.chain.runtime.TriggerScheduler;
|
||||||
|
import com.easyagents.flow.core.node.BaseNode;
|
||||||
|
import com.easyagents.flow.core.node.EndNode;
|
||||||
|
import com.easyagents.flow.core.node.StartNode;
|
||||||
|
import com.easyagents.flow.core.parser.ChainParser;
|
||||||
|
import com.easyagents.flow.core.parser.impl.EndNodeParser;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证普通节点的直接入边汇聚模式。
|
||||||
|
*/
|
||||||
|
public class NodeJoinModeTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldParseJoinModeAndRejectInvalidValues() {
|
||||||
|
ChainParser parser = ChainParser.builder()
|
||||||
|
.withDefaultParsers(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Assert.assertEquals(
|
||||||
|
NodeJoinMode.ANY,
|
||||||
|
parseEndNode(parser, null, null).getJoinMode());
|
||||||
|
Assert.assertEquals(
|
||||||
|
NodeJoinMode.ALL,
|
||||||
|
parseEndNode(parser, "all", null).getJoinMode());
|
||||||
|
Assert.assertEquals(
|
||||||
|
NodeJoinMode.ANY,
|
||||||
|
parseEndNode(parser, "ANY", null).getJoinMode());
|
||||||
|
|
||||||
|
assertInvalidJoinMode(() -> parseEndNode(parser, "first", null));
|
||||||
|
assertInvalidJoinMode(() -> parseEndNode(parser, "", null));
|
||||||
|
assertInvalidJoinMode(() -> parseEndNode(parser, "all", "loop"));
|
||||||
|
|
||||||
|
ProbeJoinNode node = new ProbeJoinNode(new AtomicInteger());
|
||||||
|
node.setJoinMode(NodeJoinMode.ALL);
|
||||||
|
assertInvalidJoinMode(() -> node.setParentId("loop"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldWaitForEveryInboundEdgeBeforeExecutingAndCheckingCondition()
|
||||||
|
throws Exception {
|
||||||
|
JoinFixture fixture = createJoinFixture(NodeJoinMode.ALL, true);
|
||||||
|
String instanceId = null;
|
||||||
|
try {
|
||||||
|
instanceId = fixture.executor.executeAsync(
|
||||||
|
fixture.definition.getId(), Collections.emptyMap());
|
||||||
|
Assert.assertTrue(fixture.branchACompleted.await(2, TimeUnit.SECONDS));
|
||||||
|
Assert.assertTrue(fixture.branchBStarted.await(2, TimeUnit.SECONDS));
|
||||||
|
|
||||||
|
String currentInstanceId = instanceId;
|
||||||
|
await(() -> hasTriggerEdge(
|
||||||
|
fixture.nodeStateRepository,
|
||||||
|
currentInstanceId,
|
||||||
|
"join",
|
||||||
|
"a-join"));
|
||||||
|
Assert.assertEquals(0, fixture.joinExecutions.get());
|
||||||
|
Assert.assertEquals(0, fixture.conditionChecks.get());
|
||||||
|
|
||||||
|
fixture.releaseBranchB.countDown();
|
||||||
|
ChainState finalState = awaitTerminal(
|
||||||
|
fixture.chainStateRepository, instanceId);
|
||||||
|
|
||||||
|
Assert.assertEquals(1, fixture.joinExecutions.get());
|
||||||
|
Assert.assertEquals(1, fixture.conditionChecks.get());
|
||||||
|
Assert.assertEquals("A+B", finalState.getExecuteResult().get("combined"));
|
||||||
|
Assert.assertEquals(Boolean.TRUE, finalState.getExecuteResult().get("sawBoth"));
|
||||||
|
} finally {
|
||||||
|
fixture.releaseBranchB.countDown();
|
||||||
|
fixture.scheduler.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldKeepAnyModeFirstArrivalBehavior() throws Exception {
|
||||||
|
JoinFixture fixture = createJoinFixture(NodeJoinMode.ANY, false);
|
||||||
|
try {
|
||||||
|
String instanceId = fixture.executor.executeAsync(
|
||||||
|
fixture.definition.getId(), Collections.emptyMap());
|
||||||
|
Assert.assertTrue(fixture.branchACompleted.await(2, TimeUnit.SECONDS));
|
||||||
|
Assert.assertTrue(fixture.branchBStarted.await(2, TimeUnit.SECONDS));
|
||||||
|
|
||||||
|
await(() -> fixture.joinExecutions.get() > 0);
|
||||||
|
ChainState finalState = awaitTerminal(
|
||||||
|
fixture.chainStateRepository, instanceId);
|
||||||
|
|
||||||
|
Assert.assertEquals(1, fixture.joinExecutions.get());
|
||||||
|
Assert.assertEquals(Boolean.FALSE, finalState.getExecuteResult().get("sawBoth"));
|
||||||
|
} finally {
|
||||||
|
fixture.releaseBranchB.countDown();
|
||||||
|
fixture.scheduler.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldKeepDefaultAnyRetryAndLoopBehavior() throws Exception {
|
||||||
|
ScheduledExecutorService schedulerPool =
|
||||||
|
Executors.newSingleThreadScheduledExecutor();
|
||||||
|
ExecutorService workerPool = Executors.newFixedThreadPool(3);
|
||||||
|
TriggerScheduler scheduler = new TriggerScheduler(
|
||||||
|
new InMemoryTriggerStore(), schedulerPool, workerPool, 1000L);
|
||||||
|
AtomicInteger executions = new AtomicInteger();
|
||||||
|
ChainDefinition definition = new ChainDefinition();
|
||||||
|
definition.setId("join-mode-retry-loop");
|
||||||
|
StartNode start = new StartNode();
|
||||||
|
start.setId("start");
|
||||||
|
RetryLoopNode worker = new RetryLoopNode(executions);
|
||||||
|
worker.setId("worker");
|
||||||
|
worker.setRetryEnable(true);
|
||||||
|
worker.setMaxRetryCount(1);
|
||||||
|
worker.setRetryIntervalMs(0L);
|
||||||
|
worker.setLoopEnable(true);
|
||||||
|
worker.setMaxLoopCount(2);
|
||||||
|
worker.setLoopIntervalMs(0L);
|
||||||
|
EndNode end = endNode("worker", "count", "count");
|
||||||
|
definition.addNode(start);
|
||||||
|
definition.addNode(worker);
|
||||||
|
definition.addNode(end);
|
||||||
|
definition.addEdge(edge("start-worker", "start", "worker"));
|
||||||
|
definition.addEdge(edge("worker-end", "worker", "end"));
|
||||||
|
|
||||||
|
ChainExecutor executor = new ChainExecutor(
|
||||||
|
ignored -> definition,
|
||||||
|
new InMemoryChainStateRepository(),
|
||||||
|
new InMemoryNodeStateRepository(),
|
||||||
|
scheduler);
|
||||||
|
try {
|
||||||
|
Map<String, Object> result = executor.execute(
|
||||||
|
definition.getId(), Collections.emptyMap(), 5L, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
Assert.assertEquals(NodeJoinMode.ANY, worker.getJoinMode());
|
||||||
|
Assert.assertEquals(3, executions.get());
|
||||||
|
Assert.assertEquals(3, result.get("count"));
|
||||||
|
} finally {
|
||||||
|
scheduler.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JoinFixture createJoinFixture(
|
||||||
|
NodeJoinMode joinMode, boolean withCondition) {
|
||||||
|
JoinFixture fixture = new JoinFixture();
|
||||||
|
fixture.schedulerPool = Executors.newScheduledThreadPool(2);
|
||||||
|
fixture.workerPool = Executors.newFixedThreadPool(4);
|
||||||
|
fixture.scheduler = new TriggerScheduler(
|
||||||
|
new InMemoryTriggerStore(),
|
||||||
|
fixture.schedulerPool,
|
||||||
|
fixture.workerPool,
|
||||||
|
1000L);
|
||||||
|
fixture.chainStateRepository = new InMemoryChainStateRepository();
|
||||||
|
fixture.nodeStateRepository = new InMemoryNodeStateRepository();
|
||||||
|
fixture.definition = new ChainDefinition();
|
||||||
|
fixture.definition.setId("join-mode-" + joinMode.getValue());
|
||||||
|
|
||||||
|
StartNode start = new StartNode();
|
||||||
|
start.setId("start");
|
||||||
|
BranchNode branchA = new BranchNode(
|
||||||
|
"A", fixture.branchACompleted, null, null);
|
||||||
|
branchA.setId("a");
|
||||||
|
BranchNode branchB = new BranchNode(
|
||||||
|
"B", null, fixture.branchBStarted, fixture.releaseBranchB);
|
||||||
|
branchB.setId("b");
|
||||||
|
ProbeJoinNode join = new ProbeJoinNode(fixture.joinExecutions);
|
||||||
|
join.setId("join");
|
||||||
|
join.setJoinMode(joinMode);
|
||||||
|
if (withCondition) {
|
||||||
|
join.setCondition(new BothOutputsCondition(fixture.conditionChecks));
|
||||||
|
}
|
||||||
|
EndNode end = endNode("join", "combined", "combined");
|
||||||
|
end.addOutputDef(outputRef("sawBoth", "join.sawBoth"));
|
||||||
|
|
||||||
|
fixture.definition.addNode(start);
|
||||||
|
fixture.definition.addNode(branchA);
|
||||||
|
fixture.definition.addNode(branchB);
|
||||||
|
fixture.definition.addNode(join);
|
||||||
|
fixture.definition.addNode(end);
|
||||||
|
fixture.definition.addEdge(edge("start-a", "start", "a"));
|
||||||
|
fixture.definition.addEdge(edge("start-b", "start", "b"));
|
||||||
|
fixture.definition.addEdge(edge("a-join", "a", "join"));
|
||||||
|
fixture.definition.addEdge(edge("b-join", "b", "join"));
|
||||||
|
fixture.definition.addEdge(edge("join-end", "join", "end"));
|
||||||
|
|
||||||
|
fixture.executor = new ChainExecutor(
|
||||||
|
ignored -> fixture.definition,
|
||||||
|
fixture.chainStateRepository,
|
||||||
|
fixture.nodeStateRepository,
|
||||||
|
fixture.scheduler);
|
||||||
|
return fixture;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node parseEndNode(
|
||||||
|
ChainParser parser, String joinMode, String parentId) {
|
||||||
|
JSONObject data = new JSONObject();
|
||||||
|
if (joinMode != null) {
|
||||||
|
data.put("joinMode", joinMode);
|
||||||
|
}
|
||||||
|
JSONObject nodeJson = new JSONObject();
|
||||||
|
nodeJson.put("id", "end");
|
||||||
|
nodeJson.put("type", "endNode");
|
||||||
|
nodeJson.put("parentId", parentId);
|
||||||
|
nodeJson.put("data", data);
|
||||||
|
return new EndNodeParser().parse(
|
||||||
|
nodeJson, new JSONObject(), parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertInvalidJoinMode(Runnable action) {
|
||||||
|
try {
|
||||||
|
action.run();
|
||||||
|
Assert.fail("Expected invalid join mode");
|
||||||
|
} catch (IllegalArgumentException expected) {
|
||||||
|
Assert.assertTrue(expected.getMessage().contains("joinMode"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasTriggerEdge(
|
||||||
|
InMemoryNodeStateRepository repository,
|
||||||
|
String instanceId,
|
||||||
|
String nodeId,
|
||||||
|
String edgeId) {
|
||||||
|
NodeState state = repository.load(instanceId, nodeId);
|
||||||
|
return state != null && state.getTriggerEdgeIds().contains(edgeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ChainState awaitTerminal(
|
||||||
|
InMemoryChainStateRepository repository,
|
||||||
|
String instanceId) throws Exception {
|
||||||
|
await(() -> {
|
||||||
|
ChainState state = repository.load(instanceId);
|
||||||
|
return state != null
|
||||||
|
&& state.getStatus() != null
|
||||||
|
&& state.getStatus().isTerminal();
|
||||||
|
});
|
||||||
|
ChainState state = repository.load(instanceId);
|
||||||
|
Assert.assertEquals(ChainStatus.SUCCEEDED, state.getStatus());
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void await(BooleanSupplier condition) throws Exception {
|
||||||
|
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(3L);
|
||||||
|
while (!condition.getAsBoolean() && System.nanoTime() < deadline) {
|
||||||
|
Thread.sleep(10L);
|
||||||
|
}
|
||||||
|
Assert.assertTrue("condition was not met before timeout", condition.getAsBoolean());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EndNode endNode(
|
||||||
|
String sourceNodeId, String sourceName, String outputName) {
|
||||||
|
EndNode end = new EndNode();
|
||||||
|
end.setId("end");
|
||||||
|
end.addOutputDef(outputRef(
|
||||||
|
outputName, sourceNodeId + "." + sourceName));
|
||||||
|
return end;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Parameter outputRef(String name, String ref) {
|
||||||
|
Parameter parameter = new Parameter();
|
||||||
|
parameter.setName(name);
|
||||||
|
parameter.setRef(ref);
|
||||||
|
parameter.setRefType(RefType.REF);
|
||||||
|
return parameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Edge edge(String id, String source, String target) {
|
||||||
|
Edge edge = new Edge();
|
||||||
|
edge.setId(id);
|
||||||
|
edge.setSource(source);
|
||||||
|
edge.setTarget(target);
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class BranchNode extends BaseNode {
|
||||||
|
private final String value;
|
||||||
|
private final CountDownLatch completed;
|
||||||
|
private final CountDownLatch started;
|
||||||
|
private final CountDownLatch release;
|
||||||
|
|
||||||
|
private BranchNode(
|
||||||
|
String value,
|
||||||
|
CountDownLatch completed,
|
||||||
|
CountDownLatch started,
|
||||||
|
CountDownLatch release) {
|
||||||
|
this.value = value;
|
||||||
|
this.completed = completed;
|
||||||
|
this.started = started;
|
||||||
|
this.release = release;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> execute(Chain chain) {
|
||||||
|
if (started != null) {
|
||||||
|
started.countDown();
|
||||||
|
}
|
||||||
|
if (release != null) {
|
||||||
|
try {
|
||||||
|
if (!release.await(3L, TimeUnit.SECONDS)) {
|
||||||
|
throw new IllegalStateException("branch release timed out");
|
||||||
|
}
|
||||||
|
} catch (InterruptedException error) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IllegalStateException("branch interrupted", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (completed != null) {
|
||||||
|
completed.countDown();
|
||||||
|
}
|
||||||
|
return Collections.singletonMap("value", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ProbeJoinNode extends BaseNode {
|
||||||
|
private final AtomicInteger executions;
|
||||||
|
|
||||||
|
private ProbeJoinNode(AtomicInteger executions) {
|
||||||
|
this.executions = executions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> execute(Chain chain) {
|
||||||
|
executions.incrementAndGet();
|
||||||
|
Object a = chain.getExecutionState().getMemory().get("a.value");
|
||||||
|
Object b = chain.getExecutionState().getMemory().get("b.value");
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("combined", String.valueOf(a) + "+" + String.valueOf(b));
|
||||||
|
result.put("sawBoth", a != null && b != null);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class BothOutputsCondition implements NodeCondition {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private final AtomicInteger checks;
|
||||||
|
|
||||||
|
private BothOutputsCondition(AtomicInteger checks) {
|
||||||
|
this.checks = checks;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean check(
|
||||||
|
Chain chain,
|
||||||
|
NodeState context,
|
||||||
|
Map<String, Object> executeResult) {
|
||||||
|
checks.incrementAndGet();
|
||||||
|
Map<String, Object> memory = chain.getExecutionState().getMemory();
|
||||||
|
return memory.containsKey("a.value") && memory.containsKey("b.value");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RetryLoopNode extends BaseNode {
|
||||||
|
private final AtomicInteger executions;
|
||||||
|
|
||||||
|
private RetryLoopNode(AtomicInteger executions) {
|
||||||
|
this.executions = executions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> execute(Chain chain) {
|
||||||
|
int count = executions.incrementAndGet();
|
||||||
|
if (count == 1) {
|
||||||
|
throw new IllegalStateException("retry once");
|
||||||
|
}
|
||||||
|
return Collections.singletonMap("count", count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class JoinFixture {
|
||||||
|
private final CountDownLatch branchACompleted = new CountDownLatch(1);
|
||||||
|
private final CountDownLatch branchBStarted = new CountDownLatch(1);
|
||||||
|
private final CountDownLatch releaseBranchB = new CountDownLatch(1);
|
||||||
|
private final AtomicInteger joinExecutions = new AtomicInteger();
|
||||||
|
private final AtomicInteger conditionChecks = new AtomicInteger();
|
||||||
|
private ScheduledExecutorService schedulerPool;
|
||||||
|
private ExecutorService workerPool;
|
||||||
|
private TriggerScheduler scheduler;
|
||||||
|
private InMemoryChainStateRepository chainStateRepository;
|
||||||
|
private InMemoryNodeStateRepository nodeStateRepository;
|
||||||
|
private ChainDefinition definition;
|
||||||
|
private ChainExecutor executor;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user