perf: 优化工作流执行引擎与循环调度
- 引入增量状态、定义快照和持久化触发器 - 收敛循环结果、模板条件和高 IO 执行开销 - 统一循环 1 至 300 次约束并补充并发回归测试
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -20,16 +20,25 @@ import com.easyagents.flow.core.util.StringUtil;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
|
||||
public class ChainDefinition implements Serializable {
|
||||
private static final long serialVersionUID = -3183115191738959423L;
|
||||
|
||||
protected String id;
|
||||
protected String name;
|
||||
protected String description;
|
||||
protected List<Node> nodes;
|
||||
protected List<Edge> edges;
|
||||
/**
|
||||
* 由节点和边派生的只读图索引,不参与序列化。
|
||||
*/
|
||||
private transient volatile GraphIndex graphIndex;
|
||||
|
||||
public ChainDefinition() {
|
||||
}
|
||||
@@ -64,6 +73,7 @@ public class ChainDefinition implements Serializable {
|
||||
|
||||
public void setNodes(List<Node> nodes) {
|
||||
this.nodes = nodes;
|
||||
invalidateGraphIndex();
|
||||
}
|
||||
|
||||
public List<Edge> getEdges() {
|
||||
@@ -72,27 +82,45 @@ public class ChainDefinition implements Serializable {
|
||||
|
||||
public void setEdges(List<Edge> edges) {
|
||||
this.edges = edges;
|
||||
invalidateGraphIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定节点的全部出边。
|
||||
*
|
||||
* @param nodeId 节点 ID
|
||||
* @return 保持定义顺序的出边副本
|
||||
*/
|
||||
public List<Edge> getOutwardEdge(String nodeId) {
|
||||
List<Edge> result = new ArrayList<>();
|
||||
for (Edge edge : edges) {
|
||||
if (nodeId.equals(edge.getSource())) {
|
||||
result.add(edge);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
List<Edge> outwardEdges = graphIndex().outwardEdgesByNode.get(nodeId);
|
||||
return outwardEdges == null ? Collections.emptyList() : new ArrayList<>(outwardEdges);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定节点的全部入边。
|
||||
*
|
||||
* @param nodeId 节点 ID
|
||||
* @return 保持定义顺序的入边副本
|
||||
*/
|
||||
public List<Edge> getInwardEdge(String nodeId) {
|
||||
List<Edge> result = new ArrayList<>();
|
||||
for (Edge edge : edges) {
|
||||
if (nodeId.equals(edge.getTarget())) {
|
||||
result.add(edge);
|
||||
List<Edge> inwardEdges = graphIndex().inwardEdgesByNode.get(nodeId);
|
||||
return inwardEdges == null ? Collections.emptyList() : new ArrayList<>(inwardEdges);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
/**
|
||||
* 获取循环节点已编译的直属分支调度描述。
|
||||
*
|
||||
* @param loopNodeId 循环节点 ID
|
||||
* @return 保持定义顺序的不可变调度描述
|
||||
*/
|
||||
public List<LoopChildDispatch> getLoopChildDispatches(
|
||||
String loopNodeId) {
|
||||
List<LoopChildDispatch> dispatches =
|
||||
graphIndex().loopChildrenByNode.get(loopNodeId);
|
||||
return dispatches == null
|
||||
? Collections.emptyList()
|
||||
: dispatches;
|
||||
}
|
||||
|
||||
public void addNode(Node node) {
|
||||
@@ -105,31 +133,21 @@ public class ChainDefinition implements Serializable {
|
||||
}
|
||||
|
||||
nodes.add(node);
|
||||
|
||||
// if (this.edges != null) {
|
||||
// for (Edge edge : edges) {
|
||||
// if (node.getId().equals(edge.getSource())) {
|
||||
// node.addOutwardEdge(edge);
|
||||
// } else if (node.getId().equals(edge.getTarget())) {
|
||||
// node.addInwardEdge(edge);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
invalidateGraphIndex();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 按 ID 获取节点。
|
||||
*
|
||||
* @param id 节点 ID
|
||||
* @return 对应节点,不存在时返回 {@code null}
|
||||
*/
|
||||
public Node getNodeById(String id) {
|
||||
if (id == null || StringUtil.noText(id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (Node node : this.nodes) {
|
||||
if (id.equals(node.getId())) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return graphIndex().nodeById.get(id);
|
||||
}
|
||||
|
||||
|
||||
@@ -138,49 +156,33 @@ public class ChainDefinition implements Serializable {
|
||||
this.edges = new ArrayList<>();
|
||||
}
|
||||
this.edges.add(edge);
|
||||
|
||||
// boolean findSource = false, findTarget = false;
|
||||
// for (Node node : this.nodes) {
|
||||
// if (node.getId().equals(edge.getSource())) {
|
||||
// node.addOutwardEdge(edge);
|
||||
// findSource = true;
|
||||
// } else if (node.getId().equals(edge.getTarget())) {
|
||||
// node.addInwardEdge(edge);
|
||||
// findTarget = true;
|
||||
// }
|
||||
// if (findSource && findTarget) {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
invalidateGraphIndex();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 按 ID 获取边。
|
||||
*
|
||||
* @param edgeId 边 ID
|
||||
* @return 对应边,不存在时返回 {@code null}
|
||||
*/
|
||||
public Edge getEdgeById(String edgeId) {
|
||||
for (Edge edge : this.edges) {
|
||||
if (edgeId.equals(edge.getId())) {
|
||||
return edge;
|
||||
}
|
||||
}
|
||||
if (StringUtil.noText(edgeId)) {
|
||||
return null;
|
||||
}
|
||||
return graphIndex().edgeById.get(edgeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取没有入边的开始节点。
|
||||
*
|
||||
* @return 保持定义顺序的开始节点副本
|
||||
*/
|
||||
public List<Node> getStartNodes() {
|
||||
if (nodes == null || nodes.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Node> result = new ArrayList<>();
|
||||
|
||||
for (Node node : nodes) {
|
||||
// if (CollectionUtil.noItems(node.getInwardEdges())) {
|
||||
// result.add(node);
|
||||
// }
|
||||
List<Edge> inwardEdge = getInwardEdge(node.getId());
|
||||
if (inwardEdge == null || inwardEdge.isEmpty()) {
|
||||
result.add(node);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return new ArrayList<>(graphIndex().startNodes);
|
||||
}
|
||||
|
||||
|
||||
@@ -198,6 +200,210 @@ public class ChainDefinition implements Serializable {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使派生图索引失效。
|
||||
*/
|
||||
private void invalidateGraphIndex() {
|
||||
graphIndex = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前节点和边对应的只读图索引。
|
||||
*
|
||||
* @return 图索引
|
||||
*/
|
||||
private GraphIndex graphIndex() {
|
||||
GraphIndex current = graphIndex;
|
||||
if (current != null) {
|
||||
return current;
|
||||
}
|
||||
synchronized (this) {
|
||||
current = graphIndex;
|
||||
if (current == null) {
|
||||
current = GraphIndex.build(nodes, edges);
|
||||
graphIndex = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作流定义的派生图索引。
|
||||
*/
|
||||
private static final class GraphIndex {
|
||||
private final Map<String, Node> nodeById;
|
||||
private final Map<String, Edge> edgeById;
|
||||
private final Map<String, List<Edge>> outwardEdgesByNode;
|
||||
private final Map<String, List<Edge>> inwardEdgesByNode;
|
||||
private final Map<String, List<LoopChildDispatch>>
|
||||
loopChildrenByNode;
|
||||
private final List<Node> startNodes;
|
||||
|
||||
/**
|
||||
* 创建不可变图索引。
|
||||
*
|
||||
* @param nodeById 节点索引
|
||||
* @param edgeById 边索引
|
||||
* @param outwardEdgesByNode 出边索引
|
||||
* @param inwardEdgesByNode 入边索引
|
||||
* @param startNodes 开始节点
|
||||
*/
|
||||
private GraphIndex(Map<String, Node> nodeById,
|
||||
Map<String, Edge> edgeById,
|
||||
Map<String, List<Edge>> outwardEdgesByNode,
|
||||
Map<String, List<Edge>> inwardEdgesByNode,
|
||||
Map<String, List<LoopChildDispatch>>
|
||||
loopChildrenByNode,
|
||||
List<Node> startNodes) {
|
||||
this.nodeById = nodeById;
|
||||
this.edgeById = edgeById;
|
||||
this.outwardEdgesByNode = outwardEdgesByNode;
|
||||
this.inwardEdgesByNode = inwardEdgesByNode;
|
||||
this.loopChildrenByNode = loopChildrenByNode;
|
||||
this.startNodes = startNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据节点和边构建索引。
|
||||
*
|
||||
* @param nodes 节点列表
|
||||
* @param edges 边列表
|
||||
* @return 构建完成的图索引
|
||||
*/
|
||||
private static GraphIndex build(List<Node> nodes, List<Edge> edges) {
|
||||
Map<String, Node> nodeById = new HashMap<>();
|
||||
Map<String, Edge> edgeById = new HashMap<>();
|
||||
Map<String, List<Edge>> outwardEdgesByNode = new HashMap<>();
|
||||
Map<String, List<Edge>> inwardEdgesByNode = new HashMap<>();
|
||||
|
||||
if (nodes != null) {
|
||||
for (Node node : nodes) {
|
||||
if (node != null && StringUtil.hasText(node.getId())) {
|
||||
// 保持旧实现遇到重复 ID 时返回第一个节点的行为。
|
||||
nodeById.putIfAbsent(node.getId(), node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (edges != null) {
|
||||
for (Edge edge : edges) {
|
||||
if (edge == null) {
|
||||
continue;
|
||||
}
|
||||
if (StringUtil.hasText(edge.getId())) {
|
||||
edgeById.putIfAbsent(edge.getId(), edge);
|
||||
}
|
||||
if (StringUtil.hasText(edge.getSource())) {
|
||||
outwardEdgesByNode
|
||||
.computeIfAbsent(edge.getSource(), ignored -> new ArrayList<>())
|
||||
.add(edge);
|
||||
}
|
||||
if (StringUtil.hasText(edge.getTarget())) {
|
||||
inwardEdgesByNode
|
||||
.computeIfAbsent(edge.getTarget(), ignored -> new ArrayList<>())
|
||||
.add(edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Node> startNodes = new ArrayList<>();
|
||||
if (nodes != null) {
|
||||
for (Node node : nodes) {
|
||||
if (node != null && !inwardEdgesByNode.containsKey(node.getId())) {
|
||||
startNodes.add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
freezeEdgeLists(outwardEdgesByNode);
|
||||
freezeEdgeLists(inwardEdgesByNode);
|
||||
Map<String, List<LoopChildDispatch>>
|
||||
loopChildrenByNode = new HashMap<>();
|
||||
for (Map.Entry<String, List<Edge>> entry :
|
||||
outwardEdgesByNode.entrySet()) {
|
||||
List<LoopChildDispatch> dispatches = new ArrayList<>();
|
||||
for (Edge edge : entry.getValue()) {
|
||||
Node child = nodeById.get(edge.getTarget());
|
||||
if (child != null
|
||||
&& Objects.equals(
|
||||
entry.getKey(), child.getParentId())) {
|
||||
String branchId = edge.getId() == null
|
||||
? child.getId()
|
||||
: edge.getId();
|
||||
dispatches.add(new LoopChildDispatch(
|
||||
child, edge.getId(), branchId));
|
||||
}
|
||||
}
|
||||
if (!dispatches.isEmpty()) {
|
||||
loopChildrenByNode.put(
|
||||
entry.getKey(),
|
||||
Collections.unmodifiableList(dispatches));
|
||||
}
|
||||
}
|
||||
return new GraphIndex(
|
||||
Collections.unmodifiableMap(nodeById),
|
||||
Collections.unmodifiableMap(edgeById),
|
||||
Collections.unmodifiableMap(outwardEdgesByNode),
|
||||
Collections.unmodifiableMap(inwardEdgesByNode),
|
||||
Collections.unmodifiableMap(loopChildrenByNode),
|
||||
Collections.unmodifiableList(startNodes));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将邻接表中的边列表转换为只读列表。
|
||||
*
|
||||
* @param edgesByNode 邻接表
|
||||
*/
|
||||
private static void freezeEdgeLists(Map<String, List<Edge>> edgesByNode) {
|
||||
edgesByNode.replaceAll((ignored, value) -> Collections.unmodifiableList(value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 循环直属分支的预编译调度描述。
|
||||
*/
|
||||
public static final class LoopChildDispatch {
|
||||
|
||||
private final Node node;
|
||||
private final String edgeId;
|
||||
private final String branchId;
|
||||
|
||||
/**
|
||||
* 创建调度描述。
|
||||
*
|
||||
* @param node 目标节点
|
||||
* @param edgeId 边 ID
|
||||
* @param branchId 稳定分支 ID
|
||||
*/
|
||||
private LoopChildDispatch(
|
||||
Node node, String edgeId, String branchId) {
|
||||
this.node = node;
|
||||
this.edgeId = edgeId;
|
||||
this.branchId = branchId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 目标节点
|
||||
*/
|
||||
public Node getNode() {
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 边 ID
|
||||
*/
|
||||
public String getEdgeId() {
|
||||
return edgeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 稳定分支 ID
|
||||
*/
|
||||
public String getBranchId() {
|
||||
return branchId;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
@@ -37,8 +37,14 @@ import java.util.stream.Collectors;
|
||||
|
||||
public class ChainState implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = -7958235553581638052L;
|
||||
|
||||
private String instanceId;
|
||||
private String parentInstanceId;
|
||||
/**
|
||||
* 节点审计应归属的顶级执行实例 ID。
|
||||
*/
|
||||
private String auditInstanceId;
|
||||
private String chainDefinitionId;
|
||||
private ConcurrentHashMap<String, Object> memory = new ConcurrentHashMap<>();
|
||||
|
||||
@@ -59,9 +65,18 @@ public class ChainState implements Serializable {
|
||||
private String message;
|
||||
private ExceptionSummary error;
|
||||
private long version;
|
||||
/**
|
||||
* 工作流实例首次启动时间,用于跨线程和跨进程执行时长保护。
|
||||
*/
|
||||
private long startedAt;
|
||||
/**
|
||||
* 已进入业务执行的节点次数,用于全局执行预算。
|
||||
*/
|
||||
private long childExecutionCount;
|
||||
|
||||
public ChainState() {
|
||||
this.instanceId = UUID.randomUUID().toString();
|
||||
this.auditInstanceId = this.instanceId;
|
||||
this.status = ChainStatus.READY;
|
||||
this.computeCost = 0;
|
||||
}
|
||||
@@ -71,7 +86,15 @@ public class ChainState implements Serializable {
|
||||
}
|
||||
|
||||
public void setInstanceId(String instanceId) {
|
||||
String previousInstanceId =
|
||||
this.instanceId;
|
||||
this.instanceId = instanceId;
|
||||
if (auditInstanceId == null
|
||||
|| Objects.equals(
|
||||
auditInstanceId,
|
||||
previousInstanceId)) {
|
||||
auditInstanceId = instanceId;
|
||||
}
|
||||
}
|
||||
|
||||
public String getParentInstanceId() {
|
||||
@@ -80,6 +103,30 @@ public class ChainState implements Serializable {
|
||||
|
||||
public void setParentInstanceId(String parentInstanceId) {
|
||||
this.parentInstanceId = parentInstanceId;
|
||||
if (StringUtil.hasText(parentInstanceId)
|
||||
&& Objects.equals(
|
||||
auditInstanceId, instanceId)) {
|
||||
auditInstanceId = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点审计归属实例 ID。
|
||||
*
|
||||
* @return 顶级审计实例 ID
|
||||
*/
|
||||
public String getAuditInstanceId() {
|
||||
return auditInstanceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置节点审计归属实例 ID。
|
||||
*
|
||||
* @param auditInstanceId 顶级审计实例 ID
|
||||
*/
|
||||
public void setAuditInstanceId(
|
||||
String auditInstanceId) {
|
||||
this.auditInstanceId = auditInstanceId;
|
||||
}
|
||||
|
||||
public String getChainDefinitionId() {
|
||||
@@ -127,8 +174,10 @@ public class ChainState implements Serializable {
|
||||
if (triggerEdgeIds == null) {
|
||||
triggerEdgeIds = new ArrayList<>();
|
||||
}
|
||||
if (!triggerEdgeIds.contains(edgeId)) {
|
||||
triggerEdgeIds.add(edgeId);
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getTriggerNodeIds() {
|
||||
return triggerNodeIds;
|
||||
@@ -142,29 +191,41 @@ public class ChainState implements Serializable {
|
||||
if (triggerNodeIds == null) {
|
||||
triggerNodeIds = new ArrayList<>();
|
||||
}
|
||||
if (!triggerNodeIds.contains(nodeId)) {
|
||||
triggerNodeIds.add(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getUncheckedEdgeIds() {
|
||||
return uncheckedEdgeIds;
|
||||
}
|
||||
|
||||
public void setUncheckedEdgeIds(List<String> uncheckedEdgeIds) {
|
||||
this.uncheckedEdgeIds = uncheckedEdgeIds;
|
||||
this.uncheckedEdgeIds = uncheckedEdgeIds == null
|
||||
? new ArrayList<>()
|
||||
: new ArrayList<>(new LinkedHashSet<>(uncheckedEdgeIds));
|
||||
}
|
||||
|
||||
public void addUncheckedEdgeId(String edgeId) {
|
||||
public boolean addUncheckedEdgeId(String edgeId) {
|
||||
if (uncheckedEdgeIds == null) {
|
||||
uncheckedEdgeIds = new ArrayList<>();
|
||||
}
|
||||
if (uncheckedEdgeIds.contains(edgeId)) {
|
||||
return false;
|
||||
}
|
||||
uncheckedEdgeIds.add(edgeId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean removeUncheckedEdgeId(String edgeId) {
|
||||
if (uncheckedEdgeIds == null) {
|
||||
return false;
|
||||
}
|
||||
return uncheckedEdgeIds.remove(edgeId);
|
||||
boolean removed = false;
|
||||
while (uncheckedEdgeIds.remove(edgeId)) {
|
||||
removed = true;
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
public List<String> getUncheckedNodeIds() {
|
||||
@@ -172,21 +233,31 @@ public class ChainState implements Serializable {
|
||||
}
|
||||
|
||||
public void setUncheckedNodeIds(List<String> uncheckedNodeIds) {
|
||||
this.uncheckedNodeIds = uncheckedNodeIds;
|
||||
this.uncheckedNodeIds = uncheckedNodeIds == null
|
||||
? new ArrayList<>()
|
||||
: new ArrayList<>(new LinkedHashSet<>(uncheckedNodeIds));
|
||||
}
|
||||
|
||||
public void addUncheckedNodeId(String nodeId) {
|
||||
public boolean addUncheckedNodeId(String nodeId) {
|
||||
if (uncheckedNodeIds == null) {
|
||||
uncheckedNodeIds = new ArrayList<>();
|
||||
}
|
||||
if (uncheckedNodeIds.contains(nodeId)) {
|
||||
return false;
|
||||
}
|
||||
uncheckedNodeIds.add(nodeId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean removeUncheckedNodeId(String nodeId) {
|
||||
if (uncheckedNodeIds == null) {
|
||||
return false;
|
||||
}
|
||||
return uncheckedNodeIds.remove(nodeId);
|
||||
boolean removed = false;
|
||||
while (uncheckedNodeIds.remove(nodeId)) {
|
||||
removed = true;
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
public Long getComputeCost() {
|
||||
@@ -281,6 +352,22 @@ public class ChainState implements Serializable {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public long getStartedAt() {
|
||||
return startedAt;
|
||||
}
|
||||
|
||||
public void setStartedAt(long startedAt) {
|
||||
this.startedAt = startedAt;
|
||||
}
|
||||
|
||||
public long getChildExecutionCount() {
|
||||
return childExecutionCount;
|
||||
}
|
||||
|
||||
public void setChildExecutionCount(long childExecutionCount) {
|
||||
this.childExecutionCount = childExecutionCount;
|
||||
}
|
||||
|
||||
public static ChainState fromJSON(String jsonString) {
|
||||
ParserConfig config = new ParserConfig();
|
||||
config.putDeserializer(ChainState.class, new ChainDeserializer());
|
||||
@@ -305,6 +392,8 @@ public class ChainState implements Serializable {
|
||||
this.status = ChainStatus.READY;
|
||||
this.message = null;
|
||||
this.error = null;
|
||||
this.startedAt = 0L;
|
||||
this.childExecutionCount = 0L;
|
||||
}
|
||||
|
||||
|
||||
@@ -340,10 +429,46 @@ public class ChainState implements Serializable {
|
||||
|
||||
|
||||
public Object resolveValue(String path) {
|
||||
return resolveValue(path, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析参数路径,并可在直接命中时保留大型结果引用。
|
||||
*
|
||||
* @param path 参数路径
|
||||
* @param preserveDirectReference 是否保留直接命中的引用
|
||||
* @return 参数值
|
||||
*/
|
||||
private Object resolveValue(
|
||||
String path,
|
||||
boolean preserveDirectReference) {
|
||||
Object result = MapUtil.getByPath(getMemory(), path);
|
||||
if (result == null) result = MapUtil.getByPath(getEnvironment(), path);
|
||||
// if (result == null) result = MapUtil.getByPath(getTriggerVariables(), path);
|
||||
return result;
|
||||
Chain chain = Chain.currentChain();
|
||||
if (result != null || chain == null || memory == null || path == null) {
|
||||
return chain == null
|
||||
|| preserveDirectReference
|
||||
? result
|
||||
: chain.resolveResultReferences(result);
|
||||
}
|
||||
|
||||
// MapUtil 无法直接穿透轻量引用,先解析最长命中的作用域值,再继续解析剩余路径。
|
||||
String[] parts = path.split("\\.");
|
||||
for (int length = parts.length - 1; length > 0; length--) {
|
||||
String prefix = String.join(".", Arrays.copyOf(parts, length));
|
||||
Object referenced = memory.get(prefix);
|
||||
if (referenced == null) {
|
||||
continue;
|
||||
}
|
||||
Object resolved = chain.resolveResultReferences(referenced);
|
||||
String remaining = String.join(
|
||||
".", Arrays.copyOfRange(parts, length, parts.length));
|
||||
return MapUtil.getByPath(
|
||||
Collections.singletonMap("value", resolved),
|
||||
"value." + remaining);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Map<String, Object> resolveParameters(Node node) {
|
||||
@@ -381,7 +506,28 @@ public class ChainState implements Serializable {
|
||||
* @return 模板渲染上下文列表
|
||||
*/
|
||||
public List<Map<String, Object>> buildTemplateRootMaps(Map<String, Object> formatArgs) {
|
||||
return Arrays.asList(getMemory(), formatArgs, getEnvMap());
|
||||
Chain chain = Chain.currentChain();
|
||||
Map<String, Object> runtimeMemory = getMemory();
|
||||
if (chain != null && runtimeMemory != null && !runtimeMemory.isEmpty()) {
|
||||
runtimeMemory = new LazyReferenceMap(
|
||||
runtimeMemory, chain);
|
||||
}
|
||||
return Arrays.asList(runtimeMemory, formatArgs, getEnvMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建审计参数使用的惰性模板上下文。
|
||||
*
|
||||
* <p>仅在模板实际读取某个 memory 顶级值时还原其中的轻量引用,
|
||||
* 避免无关固定参数同步物化大型结果。</p>
|
||||
*
|
||||
* @param formatArgs 当前节点参与模板渲染的参数
|
||||
* @return 惰性模板上下文列表
|
||||
*/
|
||||
private List<Map<String, Object>>
|
||||
buildLazyTemplateRootMaps(
|
||||
Map<String, Object> formatArgs) {
|
||||
return buildTemplateRootMaps(formatArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -403,23 +549,76 @@ public class ChainState implements Serializable {
|
||||
}
|
||||
|
||||
public Map<String, Object> resolveParameters(Node node, List<? extends Parameter> parameters, Map<String, Object> formatArgs, boolean ignoreRequired) {
|
||||
return resolveParameters(
|
||||
node,
|
||||
parameters,
|
||||
formatArgs,
|
||||
ignoreRequired,
|
||||
false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析节点审计输入,直接引用保持轻量形式,由审计消费者异步还原。
|
||||
*
|
||||
* @param node 当前节点
|
||||
* @return 兼容既有输入字段结构的参数快照
|
||||
*/
|
||||
public Map<String, Object> resolveParametersPreservingReferences(
|
||||
Node node) {
|
||||
return resolveParameters(
|
||||
node,
|
||||
node.getParameters(),
|
||||
null,
|
||||
false,
|
||||
true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析节点参数。
|
||||
*
|
||||
* @param node 当前节点
|
||||
* @param parameters 参数定义
|
||||
* @param formatArgs 模板附加参数
|
||||
* @param ignoreRequired 是否忽略必填校验
|
||||
* @param preserveDirectReferences 是否保留直接结果引用
|
||||
* @return 已解析参数
|
||||
*/
|
||||
private Map<String, Object> resolveParameters(
|
||||
Node node,
|
||||
List<? extends Parameter> parameters,
|
||||
Map<String, Object> formatArgs,
|
||||
boolean ignoreRequired,
|
||||
boolean preserveDirectReferences) {
|
||||
if (parameters == null || parameters.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, Object> variables = new LinkedHashMap<>();
|
||||
List<Parameter> suspendParameters = null;
|
||||
List<Map<String, Object>> templateRootMaps = null;
|
||||
for (Parameter parameter : parameters) {
|
||||
RefType refType = parameter.getRefType();
|
||||
Object value = null;
|
||||
if (refType == RefType.FIXED) {
|
||||
if (templateRootMaps == null) {
|
||||
templateRootMaps =
|
||||
preserveDirectReferences
|
||||
? buildLazyTemplateRootMaps(
|
||||
formatArgs)
|
||||
: buildTemplateRootMaps(
|
||||
formatArgs);
|
||||
}
|
||||
value = TextTemplate.of(parameter.getValue())
|
||||
.formatToString(buildTemplateRootMaps(formatArgs));
|
||||
.formatToString(templateRootMaps);
|
||||
} else if (refType == RefType.REF) {
|
||||
value = this.resolveValue(parameter.getRef());
|
||||
value = this.resolveValue(
|
||||
parameter.getRef(),
|
||||
preserveDirectReferences);
|
||||
}
|
||||
// 单节点执行时,参数只会传入 name 内容。
|
||||
if (value == null) {
|
||||
value = this.resolveValue(parameter.getName());
|
||||
value = this.resolveValue(
|
||||
parameter.getName(),
|
||||
preserveDirectReferences);
|
||||
}
|
||||
|
||||
if (value == null && parameter.getDefaultValue() != null) {
|
||||
@@ -475,6 +674,166 @@ public class ChainState implements Serializable {
|
||||
return variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按实际访问惰性还原顶级 memory 值的只读映射。
|
||||
*/
|
||||
private static final class LazyReferenceMap
|
||||
extends AbstractMap<String, Object> {
|
||||
|
||||
private final Map<String, Object> delegate;
|
||||
private final Chain chain;
|
||||
/**
|
||||
* 同一模板渲染内已经还原的顶级值,避免重复引用触发重复分块读取。
|
||||
*/
|
||||
private final Map<Object, Object> resolvedValues =
|
||||
new HashMap<>();
|
||||
|
||||
/**
|
||||
* 创建惰性引用映射。
|
||||
*
|
||||
* @param delegate 原始运行时 memory
|
||||
* @param chain 当前工作流链路
|
||||
*/
|
||||
private LazyReferenceMap(
|
||||
Map<String, Object> delegate,
|
||||
Chain chain) {
|
||||
this.delegate = delegate;
|
||||
this.chain = chain;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public synchronized Object get(Object key) {
|
||||
if (!delegate.containsKey(key)) {
|
||||
return null;
|
||||
}
|
||||
if (resolvedValues.containsKey(key)) {
|
||||
return resolvedValues.get(key);
|
||||
}
|
||||
Object resolved =
|
||||
chain.resolveResultReferences(
|
||||
delegate.get(key));
|
||||
resolvedValues.put(key, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
return delegate.containsKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return delegate.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int size() {
|
||||
return delegate.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Set<String> keySet() {
|
||||
return Collections.unmodifiableSet(
|
||||
delegate.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Set<Entry<String, Object>> entrySet() {
|
||||
Set<String> keys = keySet();
|
||||
return new AbstractSet<>() {
|
||||
@Override
|
||||
public Iterator<Entry<String, Object>>
|
||||
iterator() {
|
||||
Iterator<String> iterator =
|
||||
keys.iterator();
|
||||
return new Iterator<>() {
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return iterator
|
||||
.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entry<String, Object>
|
||||
next() {
|
||||
String key =
|
||||
iterator.next();
|
||||
return lazyEntry(key);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return keys.size();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建仅在读取值时还原引用的不可变条目。
|
||||
*
|
||||
* @param key memory 键
|
||||
* @return 惰性条目
|
||||
*/
|
||||
private Entry<String, Object> lazyEntry(
|
||||
String key) {
|
||||
return new Entry<>() {
|
||||
@Override
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return LazyReferenceMap.this
|
||||
.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object setValue(Object value) {
|
||||
throw new UnsupportedOperationException(
|
||||
"read-only runtime memory");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object value) {
|
||||
return value instanceof Entry<?, ?> entry
|
||||
&& Objects.equals(
|
||||
key, entry.getKey())
|
||||
&& Objects.equals(
|
||||
getValue(),
|
||||
entry.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(key)
|
||||
^ Objects.hashCode(
|
||||
getValue());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class ChainSerializer implements ObjectSerializer {
|
||||
@Override
|
||||
@@ -513,6 +872,8 @@ public class ChainState implements Serializable {
|
||||
", message='" + message + '\'' +
|
||||
", error=" + error +
|
||||
", version=" + version +
|
||||
", startedAt=" + startedAt +
|
||||
", childExecutionCount=" + childExecutionCount +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,14 @@
|
||||
package com.easyagents.flow.core.chain;
|
||||
|
||||
|
||||
public class Edge {
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 工作流节点之间的有向边定义。
|
||||
*/
|
||||
public class Edge implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String id;
|
||||
private String source;
|
||||
private String target;
|
||||
|
||||
@@ -16,10 +16,22 @@
|
||||
package com.easyagents.flow.core.chain;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
|
||||
public interface EdgeCondition {
|
||||
/**
|
||||
* 工作流边的执行条件。
|
||||
*/
|
||||
public interface EdgeCondition extends Serializable {
|
||||
|
||||
/**
|
||||
* 判断边是否允许继续执行。
|
||||
*
|
||||
* @param chain 当前工作流实例
|
||||
* @param edge 待检查的边
|
||||
* @param executeResult 上游节点执行结果
|
||||
* @return 允许执行时返回 {@code true}
|
||||
*/
|
||||
boolean check(Chain chain, Edge edge, Map<String, Object> executeResult);
|
||||
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.io.Serializable;
|
||||
import java.io.StringWriter;
|
||||
|
||||
public class ExceptionSummary implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String exceptionClass;
|
||||
private String message;
|
||||
@@ -134,4 +135,3 @@ public class ExceptionSummary implements Serializable {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,12 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class Node implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Logger log = org.slf4j.LoggerFactory.getLogger(Node.class);
|
||||
/** 可配置的最小循环次数。 */
|
||||
public static final int MIN_LOOP_COUNT = 1;
|
||||
/** 单个节点允许的最大循环次数。 */
|
||||
public static final int MAX_LOOP_COUNT = 300;
|
||||
|
||||
protected String id;
|
||||
protected String parentId;
|
||||
@@ -42,7 +47,7 @@ public abstract class Node implements Serializable {
|
||||
protected boolean loopEnable = false; // 是否启用循环执行
|
||||
protected long loopIntervalMs = 3000; // 循环间隔时间(毫秒)
|
||||
protected NodeCondition loopBreakCondition; // 跳出循环的条件
|
||||
protected int maxLoopCount = 0; // 0 表示不限制循环次数
|
||||
protected int maxLoopCount = MIN_LOOP_COUNT; // 循环总执行次数,取值范围 1~300
|
||||
|
||||
protected boolean retryEnable = false;
|
||||
protected boolean resetRetryCountAfterNormal = false;
|
||||
@@ -158,7 +163,22 @@ public abstract class Node implements Serializable {
|
||||
return maxLoopCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置节点循环的总执行次数。
|
||||
*
|
||||
* @param maxLoopCount 总执行次数,范围为 1~300
|
||||
* @throws IllegalArgumentException 循环次数超出允许范围
|
||||
*/
|
||||
public void setMaxLoopCount(int maxLoopCount) {
|
||||
if (maxLoopCount < MIN_LOOP_COUNT || maxLoopCount > MAX_LOOP_COUNT) {
|
||||
throw new IllegalArgumentException(
|
||||
"maxLoopCount must be between "
|
||||
+ MIN_LOOP_COUNT
|
||||
+ " and "
|
||||
+ MAX_LOOP_COUNT
|
||||
+ ", but was "
|
||||
+ maxLoopCount);
|
||||
}
|
||||
this.maxLoopCount = maxLoopCount;
|
||||
}
|
||||
|
||||
@@ -237,7 +257,12 @@ public abstract class Node implements Serializable {
|
||||
|
||||
protected long doCalculateComputeCost(String expr, Chain chain, Map<String, Object> result) {
|
||||
// Map<String, Object> parameterValues = chain.getState().getParameterValuesOnly(this, this.getParameters(), null);
|
||||
Map<String, Object> parameterValues = chain.getState().resolveParameters(this, this.getParameters(), null,true);
|
||||
Map<String, Object> parameterValues =
|
||||
chain.getExecutionState().resolveParameters(
|
||||
this,
|
||||
this.getParameters(),
|
||||
null,
|
||||
true);
|
||||
Map<String, Object> newMap = new HashMap<>(result);
|
||||
newMap.putAll(parameterValues);
|
||||
return JsConditionUtil.evalLong(expr, chain, newMap);
|
||||
|
||||
@@ -16,10 +16,22 @@
|
||||
package com.easyagents.flow.core.chain;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
|
||||
public interface NodeCondition {
|
||||
/**
|
||||
* 工作流节点的执行条件。
|
||||
*/
|
||||
public interface NodeCondition extends Serializable {
|
||||
|
||||
/**
|
||||
* 判断节点是否允许继续执行。
|
||||
*
|
||||
* @param chain 当前工作流实例
|
||||
* @param context 当前节点状态
|
||||
* @param executeResult 上一次执行结果
|
||||
* @return 允许执行时返回 {@code true}
|
||||
*/
|
||||
boolean check(Chain chain, NodeState context, Map<String, Object> executeResult);
|
||||
|
||||
}
|
||||
|
||||
@@ -20,10 +20,11 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class NodeState implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = -6727481826462129573L;
|
||||
|
||||
private String nodeId;
|
||||
private String chainInstanceId;
|
||||
|
||||
@@ -39,6 +40,11 @@ public class NodeState implements Serializable {
|
||||
private AtomicInteger executeCount = new AtomicInteger(0);
|
||||
private List<String> executeEdgeIds = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 当前节点生命周期对应的稳定业务尝试键。
|
||||
*/
|
||||
private String executionAttemptKey;
|
||||
|
||||
ExceptionSummary error;
|
||||
|
||||
private long version;
|
||||
@@ -135,6 +141,26 @@ public class NodeState implements Serializable {
|
||||
this.executeEdgeIds = executeEdgeIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前节点生命周期的稳定业务尝试键。
|
||||
*
|
||||
* @return 稳定业务尝试键
|
||||
*/
|
||||
public String getExecutionAttemptKey() {
|
||||
return executionAttemptKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前节点生命周期的稳定业务尝试键。
|
||||
*
|
||||
* @param executionAttemptKey 稳定业务尝试键
|
||||
*/
|
||||
public void setExecutionAttemptKey(
|
||||
String executionAttemptKey) {
|
||||
this.executionAttemptKey =
|
||||
executionAttemptKey;
|
||||
}
|
||||
|
||||
public ExceptionSummary getError() {
|
||||
return error;
|
||||
}
|
||||
@@ -158,10 +184,16 @@ public class NodeState implements Serializable {
|
||||
return true;
|
||||
}
|
||||
|
||||
List<String> shouldBeTriggerIds = inwardEdges.stream().map(Edge::getId).collect(Collectors.toList());
|
||||
List<String> triggerEdgeIds = this.triggerEdgeIds;
|
||||
return triggerEdgeIds.size() >= shouldBeTriggerIds.size()
|
||||
&& shouldBeTriggerIds.parallelStream().allMatch(triggerEdgeIds::contains);
|
||||
if (triggerEdgeIds.size() < inwardEdges.size()) {
|
||||
return false;
|
||||
}
|
||||
java.util.Set<String> triggeredEdges = new java.util.HashSet<>(triggerEdgeIds);
|
||||
for (Edge inwardEdge : inwardEdges) {
|
||||
if (!triggeredEdges.contains(inwardEdge.getId())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void recordTrigger(String fromEdgeId) {
|
||||
@@ -169,14 +201,17 @@ public class NodeState implements Serializable {
|
||||
if (fromEdgeId == null) {
|
||||
fromEdgeId = "none";
|
||||
}
|
||||
if (!triggerEdgeIds.contains(fromEdgeId)) {
|
||||
triggerEdgeIds.add(fromEdgeId);
|
||||
}
|
||||
}
|
||||
|
||||
public void recordExecute(String fromEdgeId) {
|
||||
executeCount.incrementAndGet();
|
||||
if (fromEdgeId == null) {
|
||||
fromEdgeId = "none";
|
||||
}
|
||||
executeEdgeIds.clear();
|
||||
executeEdgeIds.add(fromEdgeId);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,18 @@
|
||||
package com.easyagents.flow.core.chain;
|
||||
|
||||
|
||||
public interface NodeValidator {
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 工作流节点定义校验器。
|
||||
*/
|
||||
public interface NodeValidator extends Serializable {
|
||||
|
||||
/**
|
||||
* 校验节点定义。
|
||||
*
|
||||
* @param node 待校验节点
|
||||
* @return 校验结果
|
||||
*/
|
||||
NodeValidResult validate(Node node);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class Parameter implements Serializable, Cloneable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
protected String id;
|
||||
protected String name;
|
||||
protected String description;
|
||||
|
||||
@@ -18,40 +18,133 @@ package com.easyagents.flow.core.chain.event;
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.Node;
|
||||
import com.easyagents.flow.core.chain.NodeStatus;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 节点结束执行事件。
|
||||
*/
|
||||
public class NodeEndEvent extends BaseEvent {
|
||||
|
||||
private final Node node;
|
||||
private final Map<String, Object> result;
|
||||
private final Throwable error;
|
||||
private final NodeStatus status;
|
||||
private final String executionAttemptKey;
|
||||
|
||||
/**
|
||||
* 创建节点结束事件。
|
||||
*
|
||||
* @param chain 当前工作流
|
||||
* @param node 当前节点
|
||||
* @param result 节点输出
|
||||
* @param error 节点异常
|
||||
*/
|
||||
public NodeEndEvent(Chain chain, Node node, Map<String, Object> result, Throwable error) {
|
||||
this(chain, node, result, error, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建携带不可变业务尝试键的节点结束事件。
|
||||
*
|
||||
* @param chain 当前工作流
|
||||
* @param node 当前节点
|
||||
* @param result 节点输出
|
||||
* @param error 节点异常
|
||||
* @param executionAttemptKey 节点本次业务尝试键
|
||||
*/
|
||||
public NodeEndEvent(Chain chain,
|
||||
Node node,
|
||||
Map<String, Object> result,
|
||||
Throwable error,
|
||||
String executionAttemptKey) {
|
||||
this(
|
||||
chain,
|
||||
node,
|
||||
result,
|
||||
error,
|
||||
null,
|
||||
executionAttemptKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建携带不可变节点终态和业务尝试键的节点结束事件。
|
||||
*
|
||||
* @param chain 当前工作流
|
||||
* @param node 当前节点
|
||||
* @param result 节点输出
|
||||
* @param error 节点异常
|
||||
* @param status 节点本次业务尝试终态
|
||||
* @param executionAttemptKey 节点本次业务尝试键
|
||||
*/
|
||||
public NodeEndEvent(Chain chain,
|
||||
Node node,
|
||||
Map<String, Object> result,
|
||||
Throwable error,
|
||||
NodeStatus status,
|
||||
String executionAttemptKey) {
|
||||
super(chain);
|
||||
this.node = node;
|
||||
this.result = result;
|
||||
this.error = error;
|
||||
this.status = status;
|
||||
this.executionAttemptKey = executionAttemptKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前节点。
|
||||
*
|
||||
* @return 当前节点
|
||||
*/
|
||||
public Node getNode() {
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点输出。
|
||||
*
|
||||
* @return 节点输出
|
||||
*/
|
||||
public Map<String, Object> getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点异常。
|
||||
*
|
||||
* @return 节点异常;成功时为 {@code null}
|
||||
*/
|
||||
public Throwable getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件创建时捕获的节点终态。
|
||||
*
|
||||
* @return 节点终态;旧调用方未提供时为 {@code null}
|
||||
*/
|
||||
public NodeStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件创建时捕获的业务尝试键。
|
||||
*
|
||||
* @return 业务尝试键;旧调用方未提供时为 {@code null}
|
||||
*/
|
||||
public String getExecutionAttemptKey() {
|
||||
return executionAttemptKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "NodeEndEvent{" +
|
||||
"node=" + node +
|
||||
", result=" + result +
|
||||
", error=" + error +
|
||||
", status=" + status +
|
||||
", executionAttemptKey='" + executionAttemptKey + '\'' +
|
||||
", chain=" + chain +
|
||||
'}';
|
||||
}
|
||||
|
||||
@@ -18,25 +18,123 @@ package com.easyagents.flow.core.chain.event;
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.Node;
|
||||
import com.easyagents.flow.core.chain.NodeStatus;
|
||||
|
||||
/**
|
||||
* 节点开始执行事件。
|
||||
*/
|
||||
public class NodeStartEvent extends BaseEvent {
|
||||
|
||||
private final Node node;
|
||||
private final String executionAttemptKey;
|
||||
private final NodeStatus status;
|
||||
private final String auditInstanceId;
|
||||
|
||||
/**
|
||||
* 创建节点开始事件。
|
||||
*
|
||||
* @param chain 当前工作流
|
||||
* @param node 当前节点
|
||||
*/
|
||||
public NodeStartEvent(Chain chain, Node node) {
|
||||
super(chain);
|
||||
this.node = node;
|
||||
this(chain, node, null, null,
|
||||
chain.getStateInstanceId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建携带不可变业务尝试键的节点开始事件。
|
||||
*
|
||||
* @param chain 当前工作流
|
||||
* @param node 当前节点
|
||||
* @param executionAttemptKey 节点本次业务尝试键
|
||||
*/
|
||||
public NodeStartEvent(Chain chain,
|
||||
Node node,
|
||||
String executionAttemptKey) {
|
||||
this(chain, node, executionAttemptKey, null,
|
||||
chain.getStateInstanceId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建携带不可变业务尝试键和节点状态的开始事件。
|
||||
*
|
||||
* @param chain 当前工作流
|
||||
* @param node 当前节点
|
||||
* @param executionAttemptKey 节点本次业务尝试键
|
||||
* @param status 事件创建时的节点状态
|
||||
*/
|
||||
public NodeStartEvent(Chain chain,
|
||||
Node node,
|
||||
String executionAttemptKey,
|
||||
NodeStatus status) {
|
||||
this(chain, node, executionAttemptKey, status,
|
||||
chain.getStateInstanceId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建携带完整不可变审计上下文的节点开始事件。
|
||||
*
|
||||
* @param chain 当前工作流
|
||||
* @param node 当前节点
|
||||
* @param executionAttemptKey 节点本次业务尝试键
|
||||
* @param status 事件创建时的节点状态
|
||||
* @param auditInstanceId 节点审计应关联的顶级执行实例 ID
|
||||
*/
|
||||
public NodeStartEvent(Chain chain,
|
||||
Node node,
|
||||
String executionAttemptKey,
|
||||
NodeStatus status,
|
||||
String auditInstanceId) {
|
||||
super(chain);
|
||||
this.node = node;
|
||||
this.executionAttemptKey = executionAttemptKey;
|
||||
this.status = status;
|
||||
this.auditInstanceId = auditInstanceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前节点。
|
||||
*
|
||||
* @return 当前节点
|
||||
*/
|
||||
public Node getNode() {
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件创建时捕获的业务尝试键。
|
||||
*
|
||||
* @return 业务尝试键;旧调用方未提供时为 {@code null}
|
||||
*/
|
||||
public String getExecutionAttemptKey() {
|
||||
return executionAttemptKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件创建时捕获的节点状态。
|
||||
*
|
||||
* @return 节点状态;旧调用方未提供时为 {@code null}
|
||||
*/
|
||||
public NodeStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点审计关联的顶级执行实例 ID。
|
||||
*
|
||||
* @return 顶级执行实例 ID
|
||||
*/
|
||||
public String getAuditInstanceId() {
|
||||
return auditInstanceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "NodeStartEvent{" +
|
||||
"node=" + node +
|
||||
", executionAttemptKey='" + executionAttemptKey + '\'' +
|
||||
", status=" + status +
|
||||
", auditInstanceId='" + auditInstanceId + '\'' +
|
||||
", chain=" + chain +
|
||||
'}';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* Licensed under the GNU Lesser General Public License (LGPL), Version 3.0.
|
||||
*/
|
||||
package com.easyagents.flow.core.chain.repository;
|
||||
|
||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
|
||||
/**
|
||||
* 工作流实例级定义快照仓储。
|
||||
*/
|
||||
public interface ChainDefinitionSnapshotRepository {
|
||||
|
||||
/**
|
||||
* 保存实例启动时的定义快照。
|
||||
*
|
||||
* @param instanceId 工作流实例 ID
|
||||
* @param definition 定义快照
|
||||
*/
|
||||
void save(String instanceId, ChainDefinition definition);
|
||||
|
||||
/**
|
||||
* 加载实例启动时的定义快照。
|
||||
*
|
||||
* @param instanceId 工作流实例 ID
|
||||
* @return 定义快照;不存在时返回 null
|
||||
*/
|
||||
ChainDefinition load(String instanceId);
|
||||
|
||||
/**
|
||||
* 删除定义快照。
|
||||
*
|
||||
* @param instanceId 工作流实例 ID
|
||||
*/
|
||||
void remove(String instanceId);
|
||||
}
|
||||
@@ -25,6 +25,24 @@ public interface ChainLock extends AutoCloseable {
|
||||
*/
|
||||
boolean isAcquired();
|
||||
|
||||
/**
|
||||
* 锁是否仍由当前 owner 持有。
|
||||
*
|
||||
* @return 锁仍有效时为 true
|
||||
*/
|
||||
default boolean isValid() {
|
||||
return isAcquired();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本次锁持有期对应的 fencing token。
|
||||
*
|
||||
* @return 分布式仓储生成的单实例单调递增 token;本地锁返回 {@code 0}
|
||||
*/
|
||||
default long getFencingToken() {
|
||||
return 0L;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放锁(幂等)
|
||||
*/
|
||||
|
||||
@@ -31,8 +31,12 @@ public enum ChainStateField {
|
||||
ENVIRONMENT,
|
||||
CHILD_STATE_IDS,
|
||||
PARENT_INSTANCE_ID,
|
||||
AUDIT_INSTANCE_ID,
|
||||
TRIGGER_NODE_IDS,
|
||||
TRIGGER_EDGE_IDS,
|
||||
UNCHECKED_EDGE_IDS,
|
||||
UNCHECKED_NODE_IDS;
|
||||
UNCHECKED_NODE_IDS,
|
||||
STARTED_AT,
|
||||
CHILD_EXECUTION_COUNT,
|
||||
VERSION;
|
||||
}
|
||||
|
||||
@@ -24,8 +24,70 @@ public interface ChainStateRepository {
|
||||
|
||||
ChainState load(String instanceId);
|
||||
|
||||
/**
|
||||
* 轻量读取工作流状态版本。
|
||||
*
|
||||
* <p>分布式仓储应覆盖本方法并只读取版本字段,避免节点状态提交前反序列化完整
|
||||
* 工作流热状态。</p>
|
||||
*
|
||||
* @param instanceId 工作流实例 ID
|
||||
* @return 当前版本;状态不存在时返回 {@code null}
|
||||
*/
|
||||
default Long loadVersion(String instanceId) {
|
||||
ChainState state = load(instanceId);
|
||||
return state == null ? null : state.getVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建工作流实例状态。
|
||||
*
|
||||
* @param instanceId 工作流实例 ID
|
||||
* @return 已存在或新创建的状态
|
||||
*/
|
||||
default ChainState create(String instanceId) {
|
||||
return load(instanceId);
|
||||
}
|
||||
|
||||
boolean tryUpdate(ChainState newState, EnumSet<ChainStateField> fields);
|
||||
|
||||
/**
|
||||
* 在当前实例锁 fencing token 仍有效时提交状态。
|
||||
*
|
||||
* <p>单进程仓储可沿用普通乐观锁;分布式仓储应覆盖本方法并在同一原子操作中校验
|
||||
* token。</p>
|
||||
*
|
||||
* @param newState 待提交状态
|
||||
* @param fields 变化字段
|
||||
* @param fencingToken 当前实例锁 token;本地仓储调用为 {@code 0}
|
||||
* @return 提交成功时为 {@code true}
|
||||
*/
|
||||
default boolean tryUpdate(
|
||||
ChainState newState, EnumSet<ChainStateField> fields, long fencingToken) {
|
||||
return tryUpdate(newState, fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在实例锁和当前触发器认领租约均有效时提交状态。
|
||||
*
|
||||
* <p>分布式仓储应在同一原子操作中校验实例锁 fencing token 与 claim generation,
|
||||
* 同时拒绝锁过期后的旧执行者和租约过期后的旧 owner。</p>
|
||||
*
|
||||
* @param newState 待提交状态
|
||||
* @param fields 变化字段
|
||||
* @param lockFencingToken 当前实例锁 token;本地仓储调用为 {@code 0}
|
||||
* @param claimId 当前触发器 ID;非触发器调用为 {@code null}
|
||||
* @param claimGeneration 当前认领代际;非触发器调用为 {@code 0}
|
||||
* @return 提交成功时为 {@code true}
|
||||
*/
|
||||
default boolean tryUpdate(
|
||||
ChainState newState,
|
||||
EnumSet<ChainStateField> fields,
|
||||
long lockFencingToken,
|
||||
String claimId,
|
||||
long claimGeneration) {
|
||||
return tryUpdate(newState, fields, lockFencingToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定 instanceId 的分布式锁
|
||||
*
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* Licensed under the GNU Lesser General Public License (LGPL), Version 3.0.
|
||||
*/
|
||||
package com.easyagents.flow.core.chain.repository;
|
||||
|
||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 进程内工作流定义快照仓储。
|
||||
*/
|
||||
public class InMemoryChainDefinitionSnapshotRepository
|
||||
implements ChainDefinitionSnapshotRepository {
|
||||
|
||||
private final Map<String, ChainDefinition> snapshots = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void save(String instanceId, ChainDefinition definition) {
|
||||
snapshots.put(instanceId, definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public ChainDefinition load(String instanceId) {
|
||||
return snapshots.get(instanceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void remove(String instanceId) {
|
||||
snapshots.remove(instanceId);
|
||||
}
|
||||
}
|
||||
@@ -16,24 +16,41 @@
|
||||
package com.easyagents.flow.core.chain.repository;
|
||||
|
||||
import com.easyagents.flow.core.chain.ChainState;
|
||||
import com.easyagents.flow.core.util.MapUtil;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 进程内工作流状态仓储。
|
||||
*/
|
||||
public class InMemoryChainStateRepository implements ChainStateRepository {
|
||||
private static final Map<String, ChainState> chainStateMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public ChainState load(String instanceId) {
|
||||
return MapUtil.computeIfAbsent(chainStateMap, instanceId, k -> {
|
||||
// 保留进程内仓储原有的惰性初始化语义,兼容直接构造 Chain 的调用方式。
|
||||
return create(instanceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public ChainState create(String instanceId) {
|
||||
return chainStateMap.computeIfAbsent(instanceId, ignored -> {
|
||||
ChainState state = new ChainState();
|
||||
state.setInstanceId(instanceId);
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean tryUpdate(ChainState chainState, EnumSet<ChainStateField> fields) {
|
||||
chainStateMap.put(chainState.getInstanceId(), chainState);
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* 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.repository;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 进程内循环累计结果仓储,适用于单机运行和测试。
|
||||
*/
|
||||
public class InMemoryLoopResultRepository implements LoopResultRepository {
|
||||
|
||||
private final Map<String, Map<String, List<Object>>> results = new ConcurrentHashMap<>();
|
||||
private final Map<String, List<Object>> inputs = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int storeInput(String resultId, Iterable<?> items) {
|
||||
List<Object> stored = new ArrayList<>();
|
||||
for (Object item : items) {
|
||||
stored.add(item);
|
||||
}
|
||||
List<Object> existing = inputs.putIfAbsent(resultId, stored);
|
||||
return existing == null ? stored.size() : existing.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Object loadInputItem(String resultId, int index) {
|
||||
List<Object> stored = inputs.get(resultId);
|
||||
if (stored == null) {
|
||||
throw new IllegalStateException("Loop input not found: " + resultId);
|
||||
}
|
||||
return stored.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void removeInput(String resultId) {
|
||||
inputs.remove(resultId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void append(String resultId, int iterationIndex, Map<String, Object> outputValues) {
|
||||
if (outputValues == null || outputValues.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, List<Object>> result = results.computeIfAbsent(
|
||||
resultId, ignored -> Collections.synchronizedMap(new LinkedHashMap<>()));
|
||||
synchronized (result) {
|
||||
for (Map.Entry<String, Object> entry : outputValues.entrySet()) {
|
||||
List<Object> values = result.computeIfAbsent(entry.getKey(), ignored -> new ArrayList<>());
|
||||
if (values.size() != iterationIndex) {
|
||||
throw new IllegalStateException("Unexpected loop result index: " + iterationIndex);
|
||||
}
|
||||
values.add(entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> load(String resultId, int iterationCount, List<String> outputNames) {
|
||||
Map<String, List<Object>> result = results.get(resultId);
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||||
if (iterationCount == 0 || outputNames == null || outputNames.isEmpty()) {
|
||||
return snapshot;
|
||||
}
|
||||
if (result == null) {
|
||||
throw new IllegalStateException("Loop result not found: " + resultId);
|
||||
}
|
||||
synchronized (result) {
|
||||
for (String outputName : outputNames) {
|
||||
List<Object> values = result.get(outputName);
|
||||
if (values == null || values.size() != iterationCount) {
|
||||
throw new IllegalStateException("Incomplete loop result: " + outputName);
|
||||
}
|
||||
snapshot.put(outputName, new ArrayList<>(values));
|
||||
}
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
@@ -16,20 +16,33 @@
|
||||
package com.easyagents.flow.core.chain.repository;
|
||||
|
||||
import com.easyagents.flow.core.chain.NodeState;
|
||||
import com.easyagents.flow.core.util.MapUtil;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 进程内节点状态仓储。
|
||||
*/
|
||||
public class InMemoryNodeStateRepository implements NodeStateRepository {
|
||||
|
||||
private static final Map<String, NodeState> chainStateMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public NodeState load(String instanceId, String nodeId) {
|
||||
String key = instanceId + "." + nodeId;
|
||||
return MapUtil.computeIfAbsent(chainStateMap, key, k -> {
|
||||
// 保留进程内仓储原有的惰性初始化语义,避免改变既有直接读取行为。
|
||||
return create(instanceId, nodeId, 0L);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public NodeState create(String instanceId, String nodeId, long chainStateVersion) {
|
||||
return chainStateMap.computeIfAbsent(key(instanceId, nodeId), ignored -> {
|
||||
NodeState nodeState = new NodeState();
|
||||
nodeState.setChainInstanceId(instanceId);
|
||||
nodeState.setNodeId(nodeId);
|
||||
@@ -37,9 +50,23 @@ public class InMemoryNodeStateRepository implements NodeStateRepository {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean tryUpdate(NodeState newState, EnumSet<NodeStateField> fields, long version) {
|
||||
chainStateMap.put(newState.getChainInstanceId() + "." + newState.getNodeId(), newState);
|
||||
chainStateMap.put(key(newState.getChainInstanceId(), newState.getNodeId()), newState);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建进程内节点状态键。
|
||||
*
|
||||
* @param instanceId 工作流实例 ID
|
||||
* @param nodeId 节点 ID
|
||||
* @return 节点状态键
|
||||
*/
|
||||
private String key(String instanceId, String nodeId) {
|
||||
return instanceId + "." + nodeId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.easyagents.flow.core.chain.repository;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 已分块保存的循环输入轻量引用。
|
||||
*
|
||||
* <p>循环节点按序读取分块;其他业务节点在参数读取边界会透明还原为与原输入等价的列表。</p>
|
||||
*/
|
||||
public final class LoopInputReference implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String REFERENCE_TYPE =
|
||||
"easyflow.loop-input.v1";
|
||||
|
||||
private final String resultId;
|
||||
private final int itemCount;
|
||||
|
||||
/**
|
||||
* 创建循环输入引用。
|
||||
*
|
||||
* @param resultId 循环输入结果 ID
|
||||
* @param itemCount 输入元素数量
|
||||
*/
|
||||
public LoopInputReference(String resultId, int itemCount) {
|
||||
this.resultId = Objects.requireNonNull(
|
||||
resultId, "resultId must not be null");
|
||||
if (itemCount < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"itemCount must not be negative");
|
||||
}
|
||||
this.itemCount = itemCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取循环输入结果 ID。
|
||||
*
|
||||
* @return 结果 ID
|
||||
*/
|
||||
public String getResultId() {
|
||||
return resultId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取输入元素数量。
|
||||
*
|
||||
* @return 元素数量
|
||||
*/
|
||||
public int getItemCount() {
|
||||
return itemCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取跨异步审计边界使用的稳定引用类型。
|
||||
*
|
||||
* @return 引用类型
|
||||
*/
|
||||
public String getReferenceType() {
|
||||
return REFERENCE_TYPE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.easyagents.flow.core.chain.repository;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 循环累计输出的轻量引用,避免完整列表回写到高频热状态。
|
||||
*/
|
||||
public final class LoopResultReference implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String REFERENCE_TYPE =
|
||||
"easyflow.loop-result.v1";
|
||||
|
||||
private final String resultId;
|
||||
private final int iterationCount;
|
||||
private final String outputName;
|
||||
|
||||
/**
|
||||
* 创建循环结果引用。
|
||||
*
|
||||
* @param resultId 循环结果 ID
|
||||
* @param iterationCount 迭代次数
|
||||
* @param outputName 输出名称
|
||||
*/
|
||||
public LoopResultReference(String resultId, int iterationCount, String outputName) {
|
||||
this.resultId = Objects.requireNonNull(resultId, "resultId must not be null");
|
||||
this.iterationCount = iterationCount;
|
||||
this.outputName = Objects.requireNonNull(outputName, "outputName must not be null");
|
||||
}
|
||||
|
||||
public String getResultId() {
|
||||
return resultId;
|
||||
}
|
||||
|
||||
public int getIterationCount() {
|
||||
return iterationCount;
|
||||
}
|
||||
|
||||
public String getOutputName() {
|
||||
return outputName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取跨异步审计边界使用的稳定引用类型。
|
||||
*
|
||||
* @return 引用类型
|
||||
*/
|
||||
public String getReferenceType() {
|
||||
return REFERENCE_TYPE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,486 @@
|
||||
/**
|
||||
* 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.repository;
|
||||
|
||||
import com.easyagents.flow.core.chain.runtime.ExecutionBudgetExceededException;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 循环节点累计结果仓储。
|
||||
* <p>
|
||||
* 累计结果独立于高频更新的节点状态保存,避免每轮迭代重复序列化全部历史结果。
|
||||
*/
|
||||
public interface LoopResultRepository {
|
||||
|
||||
/**
|
||||
* 流式保存不可随机访问的循环输入。
|
||||
*
|
||||
* @param resultId 循环结果 ID
|
||||
* @param items 原始输入
|
||||
* @return 输入元素数量
|
||||
*/
|
||||
int storeInput(String resultId, Iterable<?> items);
|
||||
|
||||
/**
|
||||
* 在已启用的迭代预算内流式保存循环输入。
|
||||
*
|
||||
* <p>实现会在读取第 {@code maxItems + 1} 个元素前终止,避免超大或无限 Iterable
|
||||
* 先产生无界 I/O。具体仓储应在下游写入异常时清理已落盘的部分分块。</p>
|
||||
*
|
||||
* @param resultId 循环结果 ID
|
||||
* @param items 原始输入
|
||||
* @param maxItems 最大元素数;小于等于零表示不限制
|
||||
* @return 输入元素数量
|
||||
*/
|
||||
default int storeInput(String resultId, Iterable<?> items, long maxItems) {
|
||||
if (maxItems <= 0L) {
|
||||
return storeInput(resultId, items);
|
||||
}
|
||||
Iterable<?> bounded = () -> new Iterator<Object>() {
|
||||
private final Iterator<?> delegate = items.iterator();
|
||||
private long count;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return delegate.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object next() {
|
||||
if (count >= maxItems) {
|
||||
throw new ExecutionBudgetExceededException(
|
||||
"Loop iteration budget exceeded while storing input "
|
||||
+ resultId
|
||||
+ ": more than "
|
||||
+ maxItems);
|
||||
}
|
||||
count++;
|
||||
return delegate.next();
|
||||
}
|
||||
};
|
||||
return storeInput(resultId, bounded);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在实例锁和触发器认领均有效时流式保存循环输入。
|
||||
*
|
||||
* @param instanceId 工作流实例 ID
|
||||
* @param lockFencingToken 当前实例锁 token
|
||||
* @param claimId 当前触发器 ID
|
||||
* @param claimGeneration 当前触发器认领代际
|
||||
* @param resultId 循环结果 ID
|
||||
* @param items 原始输入
|
||||
* @param maxItems 最大元素数;小于等于零表示不限制
|
||||
* @return 输入元素数量
|
||||
*/
|
||||
default int storeInput(
|
||||
String instanceId,
|
||||
long lockFencingToken,
|
||||
String claimId,
|
||||
long claimGeneration,
|
||||
String resultId,
|
||||
Iterable<?> items,
|
||||
long maxItems) {
|
||||
return storeInput(resultId, items, maxItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在生产者主动推送数据时流式保存循环输入。
|
||||
*
|
||||
* <p>缺省实现用于本地兼容仓储;分布式仓储应覆盖此方法并边接收边分块写入,
|
||||
* 避免先构造完整列表。</p>
|
||||
*
|
||||
* @param instanceId 工作流实例 ID
|
||||
* @param lockFencingToken 当前实例锁 token
|
||||
* @param claimId 当前触发器 ID
|
||||
* @param claimGeneration 当前触发器认领代际
|
||||
* @param resultId 循环结果 ID
|
||||
* @param producer 输入生产者
|
||||
* @param maxItems 最大元素数;小于等于零表示不限制
|
||||
* @return 输入元素数量
|
||||
*/
|
||||
default int storeProducedInput(
|
||||
String instanceId,
|
||||
long lockFencingToken,
|
||||
String claimId,
|
||||
long claimGeneration,
|
||||
String resultId,
|
||||
InputProducer producer,
|
||||
long maxItems) {
|
||||
List<Object> items = new java.util.ArrayList<>();
|
||||
producer.produce(item -> {
|
||||
if (maxItems > 0L
|
||||
&& items.size() >= maxItems) {
|
||||
throw new ExecutionBudgetExceededException(
|
||||
"Loop iteration budget exceeded while storing input "
|
||||
+ resultId
|
||||
+ ": more than "
|
||||
+ maxItems);
|
||||
}
|
||||
items.add(item);
|
||||
});
|
||||
return storeInput(
|
||||
instanceId,
|
||||
lockFencingToken,
|
||||
claimId,
|
||||
claimGeneration,
|
||||
resultId,
|
||||
items,
|
||||
0L);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动向循环输入仓储推送元素的生产者。
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface InputProducer {
|
||||
|
||||
/**
|
||||
* 生产并按原顺序推送输入元素。
|
||||
*
|
||||
* @param sink 单元素接收器
|
||||
*/
|
||||
void produce(Consumer<Object> sink);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按序号读取已保存的循环输入。
|
||||
*
|
||||
* @param resultId 循环结果 ID
|
||||
* @param index 从零开始的序号
|
||||
* @return 输入元素
|
||||
*/
|
||||
Object loadInputItem(String resultId, int index);
|
||||
|
||||
/**
|
||||
* 在业务参数读取边界透明还原完整循环输入。
|
||||
*
|
||||
* @param reference 循环输入引用
|
||||
* @return 与原输入顺序一致的列表
|
||||
*/
|
||||
default List<Object> loadInput(LoopInputReference reference) {
|
||||
List<Object> items =
|
||||
new java.util.ArrayList<>(
|
||||
reference.getItemCount());
|
||||
for (int index = 0;
|
||||
index < reference.getItemCount();
|
||||
index++) {
|
||||
items.add(loadInputItem(
|
||||
reference.getResultId(), index));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理循环输入。
|
||||
*
|
||||
* @param resultId 循环结果 ID
|
||||
*/
|
||||
default void removeInput(String resultId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放指定循环结果的进程内活跃缓存。
|
||||
*
|
||||
* <p>该操作不得删除已经持久化的输入、输出分块或改变结果引用语义,仅用于在
|
||||
* 循环完成后及时归还本机缓存空间。</p>
|
||||
*
|
||||
* @param resultId 循环结果 ID
|
||||
*/
|
||||
default void releaseActiveCache(String resultId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 在实例锁和触发器认领均有效时清理循环输入。
|
||||
*
|
||||
* @param instanceId 工作流实例 ID
|
||||
* @param lockFencingToken 当前实例锁 token
|
||||
* @param claimId 当前触发器 ID
|
||||
* @param claimGeneration 当前触发器认领代际
|
||||
* @param resultId 循环结果 ID
|
||||
*/
|
||||
default void removeInput(
|
||||
String instanceId,
|
||||
long lockFencingToken,
|
||||
String claimId,
|
||||
long claimGeneration,
|
||||
String resultId) {
|
||||
removeInput(resultId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加一轮循环输出。
|
||||
*
|
||||
* @param resultId 循环结果 ID
|
||||
* @param iterationIndex 从零开始的迭代序号
|
||||
* @param outputValues 本轮输出
|
||||
*/
|
||||
void append(String resultId, int iterationIndex, Map<String, Object> outputValues);
|
||||
|
||||
/**
|
||||
* 在当前触发器 fencing token 仍有效时追加循环输出。
|
||||
*
|
||||
* @param instanceId 工作流实例 ID
|
||||
* @param fencingToken 当前触发器 token;非持久化执行为 {@code 0}
|
||||
* @param resultId 循环结果 ID
|
||||
* @param iterationIndex 迭代序号
|
||||
* @param outputValues 本轮输出
|
||||
*/
|
||||
default void append(
|
||||
String instanceId,
|
||||
long fencingToken,
|
||||
String resultId,
|
||||
int iterationIndex,
|
||||
Map<String, Object> outputValues) {
|
||||
append(resultId, iterationIndex, outputValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在实例锁和当前触发器认领租约均有效时追加循环输出。
|
||||
*
|
||||
* @param instanceId 工作流实例 ID
|
||||
* @param lockFencingToken 当前实例锁 token;本地仓储调用为 {@code 0}
|
||||
* @param claimId 当前触发器 ID;非持久化执行为 {@code null}
|
||||
* @param claimGeneration 当前认领代际;非持久化执行为 {@code 0}
|
||||
* @param resultId 循环结果 ID
|
||||
* @param iterationIndex 迭代序号
|
||||
* @param outputValues 本轮输出
|
||||
*/
|
||||
default void append(
|
||||
String instanceId,
|
||||
long lockFencingToken,
|
||||
String claimId,
|
||||
long claimGeneration,
|
||||
String resultId,
|
||||
int iterationIndex,
|
||||
Map<String, Object> outputValues) {
|
||||
append(instanceId, lockFencingToken, resultId, iterationIndex, outputValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载完整循环累计结果。
|
||||
*
|
||||
* @param resultId 循环结果 ID
|
||||
* @param iterationCount 已累计的迭代数
|
||||
* @param outputNames 输出名称,顺序与工作流定义一致
|
||||
* @return 按输出名称聚合的结果列表
|
||||
*/
|
||||
Map<String, Object> load(String resultId, int iterationCount, List<String> outputNames);
|
||||
|
||||
/**
|
||||
* 为每个循环输出创建轻量引用。
|
||||
*
|
||||
* @param resultId 循环结果 ID
|
||||
* @param iterationCount 已累计迭代数
|
||||
* @param outputNames 输出名称
|
||||
* @return 输出名称到轻量引用的映射
|
||||
*/
|
||||
default Map<String, Object> references(
|
||||
String resultId, int iterationCount, List<String> outputNames) {
|
||||
Map<String, Object> references = new LinkedHashMap<>();
|
||||
if (outputNames != null) {
|
||||
for (String outputName : outputNames) {
|
||||
references.put(
|
||||
outputName,
|
||||
new LoopResultReference(resultId, iterationCount, outputName));
|
||||
}
|
||||
}
|
||||
return references;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单个循环输出引用。
|
||||
*
|
||||
* @param reference 循环输出引用
|
||||
* @return 与旧实现相同的累计列表
|
||||
*/
|
||||
default Object resolve(LoopResultReference reference) {
|
||||
return load(
|
||||
reference.getResultId(),
|
||||
reference.getIterationCount(),
|
||||
List.of(reference.getOutputName()))
|
||||
.get(reference.getOutputName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归解析业务输出中的循环引用,供参数读取和 API 边界透明还原。
|
||||
*
|
||||
* @param value 待解析值
|
||||
* @return 不包含循环引用的业务值
|
||||
*/
|
||||
default Object resolveReferences(Object value) {
|
||||
return ReferenceResolver.resolve(this, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单次递归解析中的批量读取器。
|
||||
*/
|
||||
final class ReferenceResolver {
|
||||
|
||||
private ReferenceResolver() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集同一循环结果的全部输出名称,并按结果组批量加载一次。
|
||||
*
|
||||
* @param repository 循环结果仓储
|
||||
* @param value 待解析值
|
||||
* @return 已透明还原的值
|
||||
*/
|
||||
static Object resolve(LoopResultRepository repository, Object value) {
|
||||
Map<GroupKey, java.util.LinkedHashSet<String>> outputNames =
|
||||
new LinkedHashMap<>();
|
||||
java.util.LinkedHashMap<String, LoopInputReference> inputReferences =
|
||||
new java.util.LinkedHashMap<>();
|
||||
collect(value, outputNames, inputReferences);
|
||||
Map<GroupKey, Map<String, Object>> loaded = new LinkedHashMap<>();
|
||||
outputNames.forEach((key, names) -> loaded.put(
|
||||
key,
|
||||
repository.load(
|
||||
key.resultId,
|
||||
key.iterationCount,
|
||||
new java.util.ArrayList<>(names))));
|
||||
Map<String, List<Object>> loadedInputs =
|
||||
new LinkedHashMap<>();
|
||||
inputReferences.forEach((resultId, reference) ->
|
||||
loadedInputs.put(
|
||||
resultId,
|
||||
repository.loadInput(reference)));
|
||||
return replace(value, loaded, loadedInputs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归收集循环结果引用。
|
||||
*
|
||||
* @param value 当前值
|
||||
* @param outputNames 分组后的输出名称
|
||||
*/
|
||||
private static void collect(
|
||||
Object value,
|
||||
Map<GroupKey, java.util.LinkedHashSet<String>> outputNames,
|
||||
Map<String, LoopInputReference> inputReferences) {
|
||||
if (value instanceof LoopResultReference) {
|
||||
LoopResultReference reference = (LoopResultReference) value;
|
||||
GroupKey key = new GroupKey(
|
||||
reference.getResultId(), reference.getIterationCount());
|
||||
outputNames.computeIfAbsent(
|
||||
key, ignored -> new java.util.LinkedHashSet<>())
|
||||
.add(reference.getOutputName());
|
||||
return;
|
||||
}
|
||||
if (value instanceof LoopInputReference) {
|
||||
LoopInputReference reference =
|
||||
(LoopInputReference) value;
|
||||
inputReferences.putIfAbsent(
|
||||
reference.getResultId(), reference);
|
||||
return;
|
||||
}
|
||||
if (value instanceof Map<?, ?>) {
|
||||
((Map<?, ?>) value).values().forEach(
|
||||
item -> collect(
|
||||
item, outputNames, inputReferences));
|
||||
return;
|
||||
}
|
||||
if (value instanceof List<?>) {
|
||||
((List<?>) value).forEach(item -> collect(
|
||||
item, outputNames, inputReferences));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用已批量加载的结果递归替换引用。
|
||||
*
|
||||
* @param value 当前值
|
||||
* @param loaded 已加载结果
|
||||
* @return 替换后的值
|
||||
*/
|
||||
private static Object replace(
|
||||
Object value,
|
||||
Map<GroupKey, Map<String, Object>> loaded,
|
||||
Map<String, List<Object>> loadedInputs) {
|
||||
if (value instanceof LoopResultReference) {
|
||||
LoopResultReference reference = (LoopResultReference) value;
|
||||
Map<String, Object> outputs = loaded.get(new GroupKey(
|
||||
reference.getResultId(), reference.getIterationCount()));
|
||||
return outputs == null ? null : outputs.get(reference.getOutputName());
|
||||
}
|
||||
if (value instanceof LoopInputReference) {
|
||||
return loadedInputs.get(
|
||||
((LoopInputReference) value)
|
||||
.getResultId());
|
||||
}
|
||||
if (value instanceof Map<?, ?>) {
|
||||
Map<Object, Object> resolved = new LinkedHashMap<>();
|
||||
((Map<?, ?>) value).forEach(
|
||||
(key, item) -> resolved.put(
|
||||
key,
|
||||
replace(
|
||||
item,
|
||||
loaded,
|
||||
loadedInputs)));
|
||||
return resolved;
|
||||
}
|
||||
if (value instanceof List<?>) {
|
||||
List<?> list = (List<?>) value;
|
||||
java.util.ArrayList<Object> resolved =
|
||||
new java.util.ArrayList<>(list.size());
|
||||
for (Object item : list) {
|
||||
resolved.add(replace(
|
||||
item, loaded, loadedInputs));
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 循环结果批量读取分组键。
|
||||
*/
|
||||
private static final class GroupKey {
|
||||
|
||||
private final String resultId;
|
||||
private final int iterationCount;
|
||||
|
||||
private GroupKey(String resultId, int iterationCount) {
|
||||
this.resultId = resultId;
|
||||
this.iterationCount = iterationCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof GroupKey)) {
|
||||
return false;
|
||||
}
|
||||
GroupKey that = (GroupKey) other;
|
||||
return iterationCount == that.iterationCount
|
||||
&& java.util.Objects.equals(resultId, that.resultId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return java.util.Objects.hash(resultId, iterationCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,5 +27,7 @@ public enum NodeStateField {
|
||||
SUSPEND_NODE_IDS,
|
||||
SUSPEND_FOR_PARAMETERS,
|
||||
EXECUTE_RESULT,
|
||||
RETRY_COUNT, EXECUTE_COUNT, EXECUTE_EDGE_IDS, LOOP_COUNT, TRIGGER_COUNT, TRIGGER_EDGE_IDS, ENVIRONMENT
|
||||
RETRY_COUNT, EXECUTE_COUNT, EXECUTE_EDGE_IDS, EXECUTION_ATTEMPT_KEY,
|
||||
LOOP_COUNT, TRIGGER_COUNT, TRIGGER_EDGE_IDS, ENVIRONMENT,
|
||||
VERSION
|
||||
}
|
||||
|
||||
@@ -21,7 +21,123 @@ import java.util.EnumSet;
|
||||
|
||||
public interface NodeStateRepository {
|
||||
|
||||
/**
|
||||
* 加载已存在的节点状态。
|
||||
*
|
||||
* @param instanceId 工作流实例 ID
|
||||
* @param nodeId 节点 ID
|
||||
* @return 节点状态;纯读取实现可在状态缺失时返回 {@code null},兼容实现可惰性创建
|
||||
*/
|
||||
NodeState load(String instanceId, String nodeId);
|
||||
|
||||
/**
|
||||
* 显式创建节点状态。
|
||||
*
|
||||
* <p>缺省实现兼容旧仓储中由 {@link #load(String, String)} 完成首次创建的行为。
|
||||
* 支持持久化或分布式执行的实现应覆盖本方法并原子创建状态。</p>
|
||||
*
|
||||
* @param instanceId 工作流实例 ID
|
||||
* @param nodeId 节点 ID
|
||||
* @param chainStateVersion 创建时关联的工作流状态版本
|
||||
* @return 已存在或新创建的节点状态
|
||||
*/
|
||||
default NodeState create(String instanceId, String nodeId, long chainStateVersion) {
|
||||
NodeState existing = load(instanceId, nodeId);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
NodeState created = new NodeState();
|
||||
created.setChainInstanceId(instanceId);
|
||||
created.setNodeId(nodeId);
|
||||
if (tryUpdate(
|
||||
created,
|
||||
EnumSet.noneOf(NodeStateField.class),
|
||||
chainStateVersion)) {
|
||||
return created;
|
||||
}
|
||||
return load(instanceId, nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前触发器 fencing token 仍有效时显式创建节点状态。
|
||||
*
|
||||
* @param instanceId 工作流实例 ID
|
||||
* @param nodeId 节点 ID
|
||||
* @param chainStateVersion 工作流状态版本
|
||||
* @param fencingToken 当前触发器 token;非触发器调用为 {@code 0}
|
||||
* @return 已存在或新创建的节点状态
|
||||
*/
|
||||
default NodeState create(
|
||||
String instanceId, String nodeId, long chainStateVersion, long fencingToken) {
|
||||
return create(instanceId, nodeId, chainStateVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在实例锁和当前触发器认领租约均有效时显式创建节点状态。
|
||||
*
|
||||
* @param instanceId 工作流实例 ID
|
||||
* @param nodeId 节点 ID
|
||||
* @param chainStateVersion 工作流状态版本
|
||||
* @param lockFencingToken 当前实例锁 token;本地仓储调用为 {@code 0}
|
||||
* @param claimId 当前触发器 ID;非触发器调用为 {@code null}
|
||||
* @param claimGeneration 当前认领代际;非触发器调用为 {@code 0}
|
||||
* @return 已存在或新创建的节点状态
|
||||
*/
|
||||
default NodeState create(
|
||||
String instanceId,
|
||||
String nodeId,
|
||||
long chainStateVersion,
|
||||
long lockFencingToken,
|
||||
String claimId,
|
||||
long claimGeneration) {
|
||||
return create(instanceId, nodeId, chainStateVersion, lockFencingToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按版本尝试提交节点状态。
|
||||
*
|
||||
* @param newState 待提交的新状态
|
||||
* @param fields 本次变更字段
|
||||
* @param chainStateVersion 本次提交依赖的工作流状态版本
|
||||
* @return 提交成功时为 {@code true},版本冲突时为 {@code false}
|
||||
*/
|
||||
boolean tryUpdate(NodeState newState, EnumSet<NodeStateField> fields, long chainStateVersion);
|
||||
|
||||
/**
|
||||
* 在工作流版本和 fencing token 同时有效时提交节点状态。
|
||||
*
|
||||
* @param newState 待提交节点状态
|
||||
* @param fields 变化字段
|
||||
* @param chainStateVersion 工作流状态版本
|
||||
* @param fencingToken 当前触发器 token;非触发器调用为 {@code 0}
|
||||
* @return 提交成功时为 {@code true}
|
||||
*/
|
||||
default boolean tryUpdate(
|
||||
NodeState newState,
|
||||
EnumSet<NodeStateField> fields,
|
||||
long chainStateVersion,
|
||||
long fencingToken) {
|
||||
return tryUpdate(newState, fields, chainStateVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在工作流版本、实例锁和当前触发器认领租约同时有效时提交节点状态。
|
||||
*
|
||||
* @param newState 待提交节点状态
|
||||
* @param fields 变化字段
|
||||
* @param chainStateVersion 工作流状态版本
|
||||
* @param lockFencingToken 当前实例锁 token;本地仓储调用为 {@code 0}
|
||||
* @param claimId 当前触发器 ID;非触发器调用为 {@code null}
|
||||
* @param claimGeneration 当前认领代际;非触发器调用为 {@code 0}
|
||||
* @return 提交成功时为 {@code true}
|
||||
*/
|
||||
default boolean tryUpdate(
|
||||
NodeState newState,
|
||||
EnumSet<NodeStateField> fields,
|
||||
long chainStateVersion,
|
||||
long lockFencingToken,
|
||||
String claimId,
|
||||
long claimGeneration) {
|
||||
return tryUpdate(newState, fields, chainStateVersion, lockFencingToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.easyagents.flow.core.chain.repository.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
@@ -39,24 +40,61 @@ import java.util.concurrent.*;
|
||||
public class ChainExecutor {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ChainExecutor.class);
|
||||
private static final String NESTED_DEPTH_MEMORY_KEY =
|
||||
"__tinyflow.nesting.depth";
|
||||
private static final String DEFINITION_CALL_PATH_MEMORY_KEY =
|
||||
"__tinyflow.nesting.definitionPath";
|
||||
/**
|
||||
* 子工作流触发器使用的独立执行通道。
|
||||
*/
|
||||
public static final String CHILD_WORKFLOW_EXECUTION_LANE_PREFIX =
|
||||
"child-workflow:";
|
||||
private volatile Semaphore rootChildExecutionPermits =
|
||||
new Semaphore(32, true);
|
||||
private volatile long persistentOutcomePollMillis = 500L;
|
||||
private volatile int maxChildExecutionLaneDepth =
|
||||
Integer.MAX_VALUE;
|
||||
private static final String CHILD_EXECUTION_REFERENCE_KEY =
|
||||
"__tinyflow.workflowNode.childExecution";
|
||||
/**
|
||||
* 进程内定义快照仅作为热点缓存;持久快照负责跨节点和跨进程恢复。
|
||||
*/
|
||||
private static final int MAX_ACTIVE_DEFINITIONS = 1024;
|
||||
private final ChainDefinitionRepository definitionRepository;
|
||||
private final ChainStateRepository chainStateRepository;
|
||||
private final NodeStateRepository nodeStateRepository;
|
||||
private final LoopResultRepository loopResultRepository;
|
||||
private final ChainDefinitionSnapshotRepository definitionSnapshotRepository;
|
||||
private final TriggerScheduler triggerScheduler;
|
||||
private final ExecutionBudget executionBudget;
|
||||
private final EventManager eventManager = new EventManager();
|
||||
/** 等待同步执行结果的任务,按工作流实例 ID 进行常量时间路由。 */
|
||||
private final ConcurrentMap<String, CompletableFuture<Map<String, Object>>> pendingExecutions =
|
||||
new ConcurrentHashMap<>();
|
||||
/**
|
||||
* 活跃工作流实例使用的定义快照,避免每个节点触发都重新加载和解析定义。
|
||||
*/
|
||||
private final Map<String, ChainDefinition> activeDefinitions =
|
||||
Collections.synchronizedMap(new LinkedHashMap<>(
|
||||
MAX_ACTIVE_DEFINITIONS + 1, 0.75F, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(
|
||||
Map.Entry<String, ChainDefinition> eldest) {
|
||||
return size() > MAX_ACTIVE_DEFINITIONS;
|
||||
}
|
||||
});
|
||||
|
||||
public ChainExecutor(ChainDefinitionRepository definitionRepository
|
||||
, ChainStateRepository chainStateRepository
|
||||
, NodeStateRepository nodeStateRepository
|
||||
) {
|
||||
this.definitionRepository = definitionRepository;
|
||||
this.chainStateRepository = chainStateRepository;
|
||||
this.nodeStateRepository = nodeStateRepository;
|
||||
this.triggerScheduler = ChainRuntime.triggerScheduler();
|
||||
registerRuntimeCallbacks();
|
||||
this(definitionRepository,
|
||||
chainStateRepository,
|
||||
nodeStateRepository,
|
||||
new InMemoryLoopResultRepository(),
|
||||
new InMemoryChainDefinitionSnapshotRepository(),
|
||||
ChainRuntime.triggerScheduler(),
|
||||
ExecutionBudget.defaults());
|
||||
}
|
||||
|
||||
|
||||
@@ -64,10 +102,92 @@ public class ChainExecutor {
|
||||
, ChainStateRepository chainStateRepository
|
||||
, NodeStateRepository nodeStateRepository
|
||||
, TriggerScheduler triggerScheduler) {
|
||||
this(definitionRepository,
|
||||
chainStateRepository,
|
||||
nodeStateRepository,
|
||||
new InMemoryLoopResultRepository(),
|
||||
new InMemoryChainDefinitionSnapshotRepository(),
|
||||
triggerScheduler,
|
||||
ExecutionBudget.defaults());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建使用指定调度器和资源预算的执行器。
|
||||
*
|
||||
* @param definitionRepository 工作流定义仓储
|
||||
* @param chainStateRepository 工作流状态仓储
|
||||
* @param nodeStateRepository 节点状态仓储
|
||||
* @param triggerScheduler 触发调度器
|
||||
* @param executionBudget 平台资源保护预算
|
||||
*/
|
||||
public ChainExecutor(ChainDefinitionRepository definitionRepository
|
||||
, ChainStateRepository chainStateRepository
|
||||
, NodeStateRepository nodeStateRepository
|
||||
, TriggerScheduler triggerScheduler
|
||||
, ExecutionBudget executionBudget) {
|
||||
this(definitionRepository,
|
||||
chainStateRepository,
|
||||
nodeStateRepository,
|
||||
new InMemoryLoopResultRepository(),
|
||||
new InMemoryChainDefinitionSnapshotRepository(),
|
||||
triggerScheduler,
|
||||
executionBudget);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建使用指定调度器、循环结果仓储和资源预算的执行器。
|
||||
*
|
||||
* @param definitionRepository 工作流定义仓储
|
||||
* @param chainStateRepository 工作流状态仓储
|
||||
* @param nodeStateRepository 节点状态仓储
|
||||
* @param loopResultRepository 循环累计结果仓储
|
||||
* @param triggerScheduler 触发调度器
|
||||
* @param executionBudget 平台资源保护预算
|
||||
*/
|
||||
public ChainExecutor(ChainDefinitionRepository definitionRepository
|
||||
, ChainStateRepository chainStateRepository
|
||||
, NodeStateRepository nodeStateRepository
|
||||
, LoopResultRepository loopResultRepository
|
||||
, TriggerScheduler triggerScheduler
|
||||
, ExecutionBudget executionBudget) {
|
||||
this(definitionRepository,
|
||||
chainStateRepository,
|
||||
nodeStateRepository,
|
||||
loopResultRepository,
|
||||
new InMemoryChainDefinitionSnapshotRepository(),
|
||||
triggerScheduler,
|
||||
executionBudget);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建使用持久定义快照的执行器。
|
||||
*
|
||||
* @param definitionRepository 工作流定义仓储
|
||||
* @param chainStateRepository 工作流状态仓储
|
||||
* @param nodeStateRepository 节点状态仓储
|
||||
* @param loopResultRepository 循环累计结果仓储
|
||||
* @param definitionSnapshotRepository 实例级定义快照仓储
|
||||
* @param triggerScheduler 触发调度器
|
||||
* @param executionBudget 平台资源保护预算
|
||||
*/
|
||||
public ChainExecutor(ChainDefinitionRepository definitionRepository
|
||||
, ChainStateRepository chainStateRepository
|
||||
, NodeStateRepository nodeStateRepository
|
||||
, LoopResultRepository loopResultRepository
|
||||
, ChainDefinitionSnapshotRepository definitionSnapshotRepository
|
||||
, TriggerScheduler triggerScheduler
|
||||
, ExecutionBudget executionBudget) {
|
||||
this.definitionRepository = definitionRepository;
|
||||
this.chainStateRepository = chainStateRepository;
|
||||
this.nodeStateRepository = nodeStateRepository;
|
||||
this.loopResultRepository = loopResultRepository == null
|
||||
? new InMemoryLoopResultRepository()
|
||||
: loopResultRepository;
|
||||
this.definitionSnapshotRepository = definitionSnapshotRepository == null
|
||||
? new InMemoryChainDefinitionSnapshotRepository()
|
||||
: definitionSnapshotRepository;
|
||||
this.triggerScheduler = triggerScheduler;
|
||||
this.executionBudget = executionBudget == null ? ExecutionBudget.defaults() : executionBudget;
|
||||
registerRuntimeCallbacks();
|
||||
}
|
||||
|
||||
@@ -80,33 +200,47 @@ public class ChainExecutor {
|
||||
public Map<String, Object> execute(String definitionId, Map<String, Object> variables, long timeout, TimeUnit unit) {
|
||||
Chain chain = createChain(definitionId);
|
||||
String stateInstanceId = chain.getStateInstanceId();
|
||||
CompletableFuture<Map<String, Object>> future = new CompletableFuture<>();
|
||||
|
||||
CompletableFuture<Map<String, Object>> existing = pendingExecutions.putIfAbsent(stateInstanceId, future);
|
||||
if (existing != null) {
|
||||
throw new IllegalStateException("Duplicate pending chain execution: " + stateInstanceId);
|
||||
}
|
||||
|
||||
try {
|
||||
chain.start(variables);
|
||||
Map<String, Object> result = future.get(timeout, unit);
|
||||
Map<String, Object> result = awaitPersistentOutcome(
|
||||
stateInstanceId, timeout, unit, null);
|
||||
clearDefaultStates(result);
|
||||
return result;
|
||||
} catch (TimeoutException e) {
|
||||
future.cancel(true);
|
||||
cancel(stateInstanceId, "Execution timed out");
|
||||
throw new RuntimeException("Execution timed out", e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
future.cancel(true);
|
||||
cancel(stateInstanceId, "Execution interrupted");
|
||||
throw new RuntimeException("Execution interrupted", e);
|
||||
} catch (Throwable e) {
|
||||
future.cancel(true);
|
||||
throw new RuntimeException("Execution failed", e.getCause());
|
||||
throw new RuntimeException("Execution failed", e);
|
||||
} finally {
|
||||
pendingExecutions.remove(stateInstanceId, future);
|
||||
activeDefinitions.remove(stateInstanceId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消仍在运行的工作流实例。
|
||||
*
|
||||
* <p>取消状态写入后,未开始的触发器会在执行入口短路;已经完成的外部 I/O 也会在提交
|
||||
* 结果前重新检查状态,避免继续推进下游。</p>
|
||||
*
|
||||
* @param stateInstanceId 工作流实例 ID
|
||||
* @param message 取消原因
|
||||
* @return 本次是否完成了非终态到取消状态的转换
|
||||
*/
|
||||
public boolean cancel(String stateInstanceId, String message) {
|
||||
ChainState state = chainStateRepository.load(stateInstanceId);
|
||||
if (state == null) {
|
||||
return false;
|
||||
}
|
||||
ChainDefinition definition = getDefinitionForInstance(state);
|
||||
Chain chain = configureChain(definition, stateInstanceId);
|
||||
return chain.cancel(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册工作流调度和同步结果路由回调。
|
||||
*/
|
||||
@@ -114,6 +248,79 @@ public class ChainExecutor {
|
||||
eventManager.addEventListener(ChainStatusChangeEvent.class, this::completePendingExecution);
|
||||
eventManager.addChainErrorListener(this::failPendingExecution);
|
||||
triggerScheduler.registerConsumer(this::accept);
|
||||
triggerScheduler.registerFailureListener(
|
||||
this::failDeadLetteredTrigger);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置同步子工作流的容量和持久状态轮询参数。
|
||||
*
|
||||
* <p>该方法应在执行器对外提供服务前调用。</p>
|
||||
*
|
||||
* @param rootPermits 根级同步子流程最大并发
|
||||
* @param pollMillis 持久终态轮询间隔毫秒数
|
||||
* @param laneMaxDepth 已注册独立执行通道覆盖的最大深度
|
||||
*/
|
||||
public void configureChildWorkflowRuntime(
|
||||
int rootPermits,
|
||||
long pollMillis,
|
||||
int laneMaxDepth) {
|
||||
if (rootPermits <= 0 || pollMillis <= 0L
|
||||
|| laneMaxDepth <= 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Child workflow runtime values must be positive");
|
||||
}
|
||||
this.rootChildExecutionPermits =
|
||||
new Semaphore(rootPermits, true);
|
||||
this.persistentOutcomePollMillis = pollMillis;
|
||||
this.maxChildExecutionLaneDepth = laneMaxDepth;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将已成功写入死信的触发器对应实例收敛为失败终态。
|
||||
*
|
||||
* <p>状态更新使用实例锁 fencing token,避免旧节点在锁失效后覆盖新 owner 的
|
||||
* 业务终态;终态实例保持原状态。</p>
|
||||
*
|
||||
* @param trigger 已死信触发器
|
||||
* @param failure 最后一次失败
|
||||
*/
|
||||
private boolean failDeadLetteredTrigger(
|
||||
Trigger trigger, Throwable failure) {
|
||||
if (trigger == null
|
||||
|| trigger.getStateInstanceId() == null) {
|
||||
return true;
|
||||
}
|
||||
String instanceId = trigger.getStateInstanceId();
|
||||
Throwable cause = failure == null
|
||||
? new ChainException(
|
||||
"Workflow trigger delivery attempts exhausted: "
|
||||
+ trigger.getId())
|
||||
: failure;
|
||||
try {
|
||||
ChainState state =
|
||||
chainStateRepository.load(instanceId);
|
||||
if (state == null
|
||||
|| (state.getStatus() != null
|
||||
&& state.getStatus().isTerminal())) {
|
||||
return true;
|
||||
}
|
||||
ChainDefinition definition =
|
||||
getDefinitionForInstance(state);
|
||||
if (definition == null) {
|
||||
definition = new ChainDefinition();
|
||||
definition.setId(state.getChainDefinitionId());
|
||||
}
|
||||
Chain chain = configureChain(
|
||||
definition, instanceId);
|
||||
return chain.failTerminal(cause);
|
||||
} catch (Throwable terminalError) {
|
||||
log.error(
|
||||
"Failed to mark dead-lettered workflow terminal, "
|
||||
+ "instanceId={}, triggerId={}",
|
||||
instanceId, trigger.getId(), terminalError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,20 +337,29 @@ public class ChainExecutor {
|
||||
|
||||
String stateInstanceId = chain.getStateInstanceId();
|
||||
CompletableFuture<Map<String, Object>> future = pendingExecutions.get(stateInstanceId);
|
||||
if (future == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (future != null) {
|
||||
ChainState state = chainStateRepository.load(stateInstanceId);
|
||||
if (state == null) {
|
||||
throw new ChainException("Chain state not found: " + stateInstanceId);
|
||||
}
|
||||
Map<String, Object> execResult = state.getExecuteResult();
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> execResult = (Map<String, Object>)
|
||||
loopResultRepository.resolveReferences(state.getExecuteResult());
|
||||
future.complete(execResult != null ? execResult : Collections.emptyMap());
|
||||
}
|
||||
} catch (Exception error) {
|
||||
if (future != null) {
|
||||
future.completeExceptionally(error);
|
||||
}
|
||||
log.error(
|
||||
"Failed to complete workflow execution continuation, instanceId={}",
|
||||
stateInstanceId,
|
||||
error);
|
||||
} finally {
|
||||
activeDefinitions.remove(stateInstanceId);
|
||||
definitionSnapshotRepository.remove(stateInstanceId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,10 +369,16 @@ public class ChainExecutor {
|
||||
* @param chain 发生异常的工作流实例
|
||||
*/
|
||||
private void failPendingExecution(Throwable error, Chain chain) {
|
||||
CompletableFuture<Map<String, Object>> future = pendingExecutions.get(chain.getStateInstanceId());
|
||||
String stateInstanceId = chain.getStateInstanceId();
|
||||
CompletableFuture<Map<String, Object>> future = pendingExecutions.get(stateInstanceId);
|
||||
if (future != null) {
|
||||
future.completeExceptionally(error);
|
||||
}
|
||||
activeDefinitions.remove(stateInstanceId);
|
||||
ChainState state = chainStateRepository.load(stateInstanceId);
|
||||
if (state != null && state.getStatus().isTerminal()) {
|
||||
definitionSnapshotRepository.remove(stateInstanceId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,8 +398,416 @@ public class ChainExecutor {
|
||||
|
||||
public String executeAsync(String definitionId, Map<String, Object> variables) {
|
||||
Chain chain = createChain(definitionId);
|
||||
try {
|
||||
chain.start(variables);
|
||||
return chain.getStateInstanceId();
|
||||
} catch (RuntimeException | Error error) {
|
||||
activeDefinitions.remove(chain.getStateInstanceId());
|
||||
definitionSnapshotRepository.remove(chain.getStateInstanceId());
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在独立触发执行通道中同步执行子工作流。
|
||||
*
|
||||
* <p>调用者继续获得与历史实现一致的同步结果;子流程自身的节点触发器在独立
|
||||
* worker lane 执行,因此父节点等待不会占满子流程所需的普通节点工作线程。
|
||||
* 根级子流程并发受宽松许可保护,防止异常调用一次创建过多等待线程。</p>
|
||||
*
|
||||
* @param definitionId 子流程定义 ID
|
||||
* @param variables 子流程输入
|
||||
* @param parentChain 父流程
|
||||
* @param parentNodeId 父工作流节点 ID
|
||||
* @return 子流程输出
|
||||
*/
|
||||
public Map<String, Object> executeChild(
|
||||
String definitionId,
|
||||
Map<String, Object> variables,
|
||||
Chain parentChain,
|
||||
String parentNodeId) {
|
||||
Objects.requireNonNull(parentChain, "parentChain required");
|
||||
if (parentNodeId == null || parentNodeId.isBlank()) {
|
||||
throw new IllegalArgumentException("parentNodeId required");
|
||||
}
|
||||
ChainState parentState = chainStateRepository.load(
|
||||
parentChain.getStateInstanceId());
|
||||
int parentDepth = readNestedDepth(parentState);
|
||||
int childDepth = parentDepth + 1;
|
||||
executionBudget.checkNestedDepth(parentNodeId, childDepth);
|
||||
if (childDepth > maxChildExecutionLaneDepth) {
|
||||
throw new ExecutionBudgetExceededException(
|
||||
"Child workflow depth "
|
||||
+ childDepth
|
||||
+ " exceeds registered execution lanes "
|
||||
+ maxChildExecutionLaneDepth);
|
||||
}
|
||||
List<String> callPath = readDefinitionCallPath(
|
||||
parentState, parentChain.getDefinition().getId());
|
||||
String canonicalChildId = canonicalDefinitionId(definitionId);
|
||||
if (callPath.contains(canonicalChildId)) {
|
||||
throw new ExecutionBudgetExceededException(
|
||||
"Recursive workflow call detected at "
|
||||
+ parentNodeId
|
||||
+ ": "
|
||||
+ String.join(" -> ", callPath)
|
||||
+ " -> "
|
||||
+ canonicalChildId);
|
||||
}
|
||||
List<String> childCallPath = new ArrayList<>(callPath);
|
||||
childCallPath.add(canonicalChildId);
|
||||
Trigger currentTrigger = TriggerContext.getCurrentTrigger();
|
||||
String invocationId = currentTrigger == null
|
||||
? "direct:" + UUID.randomUUID()
|
||||
: (currentTrigger.getLogicalExecutionId() == null
|
||||
|| currentTrigger.getLogicalExecutionId().isBlank()
|
||||
? currentTrigger.getId()
|
||||
: currentTrigger.getLogicalExecutionId());
|
||||
boolean rootPermit = false;
|
||||
try {
|
||||
if (parentDepth == 0) {
|
||||
if (!rootChildExecutionPermits.tryAcquire(
|
||||
1L, TimeUnit.SECONDS)) {
|
||||
throw new RetryableTriggerException(
|
||||
"子工作流并发繁忙,请稍后重试", null);
|
||||
}
|
||||
rootPermit = true;
|
||||
}
|
||||
ChildExecutionReference reference =
|
||||
prepareChildExecution(
|
||||
definitionId,
|
||||
parentChain,
|
||||
parentNodeId,
|
||||
invocationId,
|
||||
childDepth,
|
||||
childCallPath);
|
||||
startChildIfReady(reference, variables);
|
||||
Map<String, Object> result = awaitPersistentOutcome(
|
||||
reference.childInstanceId(),
|
||||
Long.MAX_VALUE,
|
||||
TimeUnit.SECONDS,
|
||||
parentChain);
|
||||
markChildExecutionCompleted(
|
||||
parentChain, parentNodeId, reference);
|
||||
clearDefaultStates(result);
|
||||
return result;
|
||||
} catch (InterruptedException error) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException(
|
||||
"Child workflow execution interrupted", error);
|
||||
} catch (TimeoutException impossible) {
|
||||
throw new IllegalStateException(
|
||||
"Unexpected child workflow timeout", impossible);
|
||||
} finally {
|
||||
if (rootPermit) {
|
||||
rootChildExecutionPermits.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在父实例短锁内复用或创建持久子流程关联。
|
||||
*
|
||||
* @param definitionId 子流程定义 ID
|
||||
* @param parentChain 父流程
|
||||
* @param parentNodeId 父节点 ID
|
||||
* @param invocationId 父节点逻辑执行 ID
|
||||
* @param childDepth 子流程综合深度
|
||||
* @param childCallPath 子流程调用链
|
||||
* @return 持久子流程关联
|
||||
*/
|
||||
private ChildExecutionReference prepareChildExecution(
|
||||
String definitionId,
|
||||
Chain parentChain,
|
||||
String parentNodeId,
|
||||
String invocationId,
|
||||
int childDepth,
|
||||
List<String> childCallPath) {
|
||||
return parentChain.executeWithLock(
|
||||
parentChain.getStateInstanceId(),
|
||||
10L,
|
||||
TimeUnit.SECONDS,
|
||||
() -> {
|
||||
NodeState nodeState =
|
||||
parentChain.updateNodeStateSafely(
|
||||
parentNodeId, state -> null);
|
||||
Object existingValue = nodeState.getMemory().get(
|
||||
CHILD_EXECUTION_REFERENCE_KEY);
|
||||
if (existingValue instanceof ChildExecutionReference) {
|
||||
ChildExecutionReference existing =
|
||||
(ChildExecutionReference) existingValue;
|
||||
if (Objects.equals(
|
||||
existing.invocationId(), invocationId)) {
|
||||
return existing;
|
||||
}
|
||||
ChainState existingChild = chainStateRepository.load(
|
||||
existing.childInstanceId());
|
||||
if (existingChild != null
|
||||
&& (existingChild.getStatus() == null
|
||||
|| !existingChild.getStatus().isTerminal())) {
|
||||
throw new RetryableTriggerException(
|
||||
"前一次子工作流仍在执行", null);
|
||||
}
|
||||
}
|
||||
Chain child = createChain(definitionId);
|
||||
child.updateStateSafely(state -> {
|
||||
state.getMemory().put(
|
||||
NESTED_DEPTH_MEMORY_KEY, childDepth);
|
||||
state.getMemory().put(
|
||||
DEFINITION_CALL_PATH_MEMORY_KEY,
|
||||
new ArrayList<>(childCallPath));
|
||||
return EnumSet.of(ChainStateField.MEMORY);
|
||||
});
|
||||
ChildExecutionReference created =
|
||||
new ChildExecutionReference(
|
||||
invocationId,
|
||||
child.getStateInstanceId(),
|
||||
childDepth,
|
||||
false);
|
||||
parentChain.updateNodeStateSafely(
|
||||
parentNodeId,
|
||||
state -> {
|
||||
state.getMemory().put(
|
||||
CHILD_EXECUTION_REFERENCE_KEY,
|
||||
created);
|
||||
return EnumSet.of(NodeStateField.MEMORY);
|
||||
});
|
||||
return created;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 在子实例仍为 READY 时幂等启动。
|
||||
*
|
||||
* @param reference 子流程关联
|
||||
* @param variables 子流程输入
|
||||
*/
|
||||
private void startChildIfReady(
|
||||
ChildExecutionReference reference,
|
||||
Map<String, Object> variables) {
|
||||
ChainState childState = chainStateRepository.load(
|
||||
reference.childInstanceId());
|
||||
if (childState == null) {
|
||||
throw new ChainException(
|
||||
"Child chain state not found: "
|
||||
+ reference.childInstanceId());
|
||||
}
|
||||
ChainDefinition definition =
|
||||
getDefinitionForInstance(childState);
|
||||
if (definition == null) {
|
||||
throw new ChainException(
|
||||
"Child chain definition not found: "
|
||||
+ reference.childInstanceId());
|
||||
}
|
||||
Chain child = configureChain(
|
||||
definition, reference.childInstanceId());
|
||||
child.setExecutionLane(childExecutionLane(reference.depth()));
|
||||
child.setNestedDepthBase(reference.depth());
|
||||
child.executeWithLock(
|
||||
reference.childInstanceId(),
|
||||
10L,
|
||||
TimeUnit.SECONDS,
|
||||
() -> {
|
||||
ChainState latest = chainStateRepository.load(
|
||||
reference.childInstanceId());
|
||||
if (latest != null
|
||||
&& (latest.getStatus() == ChainStatus.READY
|
||||
|| latest.getStatus()
|
||||
== ChainStatus.RUNNING)) {
|
||||
child.start(variables);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记当前父节点关联已观察到子流程终态。
|
||||
*
|
||||
* @param parentChain 父流程
|
||||
* @param parentNodeId 父节点 ID
|
||||
* @param reference 子流程关联
|
||||
*/
|
||||
private void markChildExecutionCompleted(
|
||||
Chain parentChain,
|
||||
String parentNodeId,
|
||||
ChildExecutionReference reference) {
|
||||
parentChain.updateNodeStateSafely(parentNodeId, state -> {
|
||||
Object current = state.getMemory().get(
|
||||
CHILD_EXECUTION_REFERENCE_KEY);
|
||||
if (!(current instanceof ChildExecutionReference)
|
||||
|| !Objects.equals(
|
||||
((ChildExecutionReference) current).invocationId(),
|
||||
reference.invocationId())) {
|
||||
return null;
|
||||
}
|
||||
state.getMemory().put(
|
||||
CHILD_EXECUTION_REFERENCE_KEY,
|
||||
new ChildExecutionReference(
|
||||
reference.invocationId(),
|
||||
reference.childInstanceId(),
|
||||
reference.depth(),
|
||||
true));
|
||||
return EnumSet.of(NodeStateField.MEMORY);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 按综合嵌套深度生成独立子流程通道。
|
||||
*
|
||||
* @param depth 综合嵌套深度
|
||||
* @return 通道名
|
||||
*/
|
||||
public static String childExecutionLane(int depth) {
|
||||
return CHILD_WORKFLOW_EXECUTION_LANE_PREFIX
|
||||
+ Math.max(1, depth);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取实例持久化的子工作流嵌套深度。
|
||||
*
|
||||
* @param state 工作流状态
|
||||
* @return 非负嵌套深度
|
||||
*/
|
||||
private int readNestedDepth(ChainState state) {
|
||||
if (state == null) {
|
||||
return 0;
|
||||
}
|
||||
Object value = state.getMemory().get(NESTED_DEPTH_MEMORY_KEY);
|
||||
return value instanceof Number
|
||||
? Math.max(0, ((Number) value).intValue())
|
||||
: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取并规范化实例的工作流定义调用链。
|
||||
*
|
||||
* @param state 工作流状态
|
||||
* @param currentDefinitionId 当前定义 ID
|
||||
* @return 从根定义到当前定义的调用链
|
||||
*/
|
||||
private List<String> readDefinitionCallPath(
|
||||
ChainState state, String currentDefinitionId) {
|
||||
List<String> path = new ArrayList<>();
|
||||
Object value = state == null
|
||||
? null
|
||||
: state.getMemory().get(DEFINITION_CALL_PATH_MEMORY_KEY);
|
||||
if (value instanceof Collection<?>) {
|
||||
for (Object item : (Collection<?>) value) {
|
||||
if (item != null) {
|
||||
path.add(canonicalDefinitionId(String.valueOf(item)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (path.isEmpty() && currentDefinitionId != null) {
|
||||
path.add(canonicalDefinitionId(currentDefinitionId));
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将发布态和草稿态的同一工作流规范化为统一调用身份。
|
||||
*
|
||||
* @param definitionId 定义 ID
|
||||
* @return 规范化定义 ID
|
||||
*/
|
||||
private String canonicalDefinitionId(String definitionId) {
|
||||
if (definitionId == null) {
|
||||
return "";
|
||||
}
|
||||
String normalized = definitionId.trim();
|
||||
return normalized.startsWith("published:")
|
||||
? normalized.substring("published:".length())
|
||||
: normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询持久状态等待工作流终态,允许任意集群实例执行实际触发器。
|
||||
*
|
||||
* @param stateInstanceId 工作流实例 ID
|
||||
* @param timeout 超时数值
|
||||
* @param unit 超时单位
|
||||
* @return 已解析业务结果
|
||||
* @throws InterruptedException 等待线程被中断
|
||||
* @throws TimeoutException 超时
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> awaitPersistentOutcome(
|
||||
String stateInstanceId,
|
||||
long timeout,
|
||||
TimeUnit unit,
|
||||
Chain parentChain)
|
||||
throws InterruptedException, TimeoutException {
|
||||
Objects.requireNonNull(unit, "time unit required");
|
||||
long timeoutNanos = timeout == Long.MAX_VALUE
|
||||
? Long.MAX_VALUE
|
||||
: Math.max(0L, unit.toNanos(timeout));
|
||||
long startedAt = System.nanoTime();
|
||||
while (true) {
|
||||
if (parentChain != null) {
|
||||
ChainState parentState = chainStateRepository.load(
|
||||
parentChain.getStateInstanceId());
|
||||
if (parentState == null
|
||||
|| (parentState.getStatus() != null
|
||||
&& parentState.getStatus().isTerminal())) {
|
||||
cancel(
|
||||
stateInstanceId,
|
||||
"Parent workflow is no longer running");
|
||||
throw new ChainException(
|
||||
"Parent workflow ended while child was running");
|
||||
}
|
||||
Trigger owner = TriggerContext.getCurrentTrigger();
|
||||
if (owner != null) {
|
||||
// 认领丢失只终止旧 owner 的等待;durable child 留给新 owner 复用。
|
||||
triggerScheduler.assertClaimOwned(owner);
|
||||
}
|
||||
}
|
||||
ChainState state = chainStateRepository.load(stateInstanceId);
|
||||
if (state == null) {
|
||||
throw new ChainException(
|
||||
"Chain state not found: " + stateInstanceId);
|
||||
}
|
||||
ChainStatus status = state.getStatus();
|
||||
if (status != null && status.isTerminal()) {
|
||||
if (!status.isSuccess()) {
|
||||
ExceptionSummary error = state.getError();
|
||||
throw new ChainException(
|
||||
error == null
|
||||
? "Workflow ended with status " + status
|
||||
: error.getMessage());
|
||||
}
|
||||
Map<String, Object> result =
|
||||
(Map<String, Object>)
|
||||
loopResultRepository.resolveReferences(
|
||||
state.getExecuteResult());
|
||||
return result == null
|
||||
? Collections.emptyMap()
|
||||
: result;
|
||||
}
|
||||
if (timeoutNanos != Long.MAX_VALUE
|
||||
&& System.nanoTime() - startedAt >= timeoutNanos) {
|
||||
throw new TimeoutException(
|
||||
"Workflow execution timed out: "
|
||||
+ stateInstanceId);
|
||||
}
|
||||
Thread.sleep(persistentOutcomePollMillis);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 父工作流节点与子实例之间的持久关联。
|
||||
*
|
||||
* @param invocationId 父节点逻辑执行 ID
|
||||
* @param childInstanceId 子流程实例 ID
|
||||
* @param depth 子流程综合嵌套深度
|
||||
* @param completed 是否已观察到终态
|
||||
*/
|
||||
private record ChildExecutionReference(
|
||||
String invocationId,
|
||||
String childInstanceId,
|
||||
int depth,
|
||||
boolean completed) implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +822,9 @@ public class ChainExecutor {
|
||||
public Map<String, Object> executeNode(String definitionId, String nodeId, Map<String, Object> variables) {
|
||||
ChainDefinition chainDefinitionById = definitionRepository.getChainDefinitionById(definitionId);
|
||||
Node node = chainDefinitionById.getNodeById(nodeId);
|
||||
Chain temp = createChain(definitionId);
|
||||
Chain temp = createChain(chainDefinitionById);
|
||||
try {
|
||||
temp.initializeState();
|
||||
if (variables != null && !variables.isEmpty()) {
|
||||
temp.updateStateSafely(s -> {
|
||||
s.getMemory().putAll(variables);
|
||||
@@ -201,6 +833,10 @@ public class ChainExecutor {
|
||||
});
|
||||
}
|
||||
return node.execute(temp);
|
||||
} finally {
|
||||
activeDefinitions.remove(temp.getStateInstanceId());
|
||||
definitionSnapshotRepository.remove(temp.getStateInstanceId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -229,17 +865,12 @@ public class ChainExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
ChainDefinition definition = definitionRepository.getChainDefinitionById(state.getChainDefinitionId());
|
||||
ChainDefinition definition = getDefinitionForInstance(state);
|
||||
if (definition == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Chain chain = new Chain(definition, state.getInstanceId());
|
||||
chain.setTriggerScheduler(triggerScheduler);
|
||||
chain.setChainStateRepository(chainStateRepository);
|
||||
chain.setNodeStateRepository(nodeStateRepository);
|
||||
chain.setEventManager(eventManager);
|
||||
|
||||
Chain chain = configureChain(definition, state.getInstanceId());
|
||||
chain.resume(variables);
|
||||
}
|
||||
|
||||
@@ -249,44 +880,132 @@ public class ChainExecutor {
|
||||
if (definition == null) {
|
||||
throw new RuntimeException("Chain definition not found");
|
||||
}
|
||||
return createChain(definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用已加载的定义创建工作流实例,避免同一次调用重复读取定义。
|
||||
*
|
||||
* @param definition 已加载的工作流定义
|
||||
* @return 已完成运行时依赖配置的工作流实例
|
||||
*/
|
||||
private Chain createChain(ChainDefinition definition) {
|
||||
String stateInstanceId = UUID.randomUUID().toString();
|
||||
activeDefinitions.put(stateInstanceId, definition);
|
||||
try {
|
||||
definitionSnapshotRepository.save(stateInstanceId, definition);
|
||||
Chain chain = configureChain(definition, stateInstanceId);
|
||||
chain.initializeState();
|
||||
return chain;
|
||||
} catch (RuntimeException | Error error) {
|
||||
activeDefinitions.remove(stateInstanceId);
|
||||
try {
|
||||
definitionSnapshotRepository.remove(stateInstanceId);
|
||||
} catch (RuntimeException cleanupError) {
|
||||
error.addSuppressed(cleanupError);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为工作流实例配置共享运行时依赖。
|
||||
*
|
||||
* @param definition 工作流定义
|
||||
* @param stateInstanceId 状态实例 ID
|
||||
* @return 配置完成的工作流实例
|
||||
*/
|
||||
private Chain configureChain(ChainDefinition definition, String stateInstanceId) {
|
||||
return configureChain(definition, stateInstanceId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为工作流实例配置共享运行时依赖,并复用调用方已经加载的状态。
|
||||
*
|
||||
* @param definition 工作流定义
|
||||
* @param stateInstanceId 状态实例 ID
|
||||
* @param persistedState 已加载状态;为 {@code null} 时按需读取
|
||||
* @return 配置完成的工作流实例
|
||||
*/
|
||||
private Chain configureChain(
|
||||
ChainDefinition definition,
|
||||
String stateInstanceId,
|
||||
ChainState persistedState) {
|
||||
Chain chain = new Chain(definition, stateInstanceId);
|
||||
chain.setTriggerScheduler(triggerScheduler);
|
||||
chain.setChainStateRepository(chainStateRepository);
|
||||
chain.setNodeStateRepository(nodeStateRepository);
|
||||
chain.setLoopResultRepository(loopResultRepository);
|
||||
chain.setEventManager(eventManager);
|
||||
|
||||
chain.setExecutionBudget(executionBudget);
|
||||
ChainState state = persistedState == null
|
||||
? chainStateRepository.load(stateInstanceId)
|
||||
: persistedState;
|
||||
int nestedDepth = readNestedDepth(state);
|
||||
chain.setNestedDepthBase(nestedDepth);
|
||||
if (nestedDepth > 0) {
|
||||
chain.setExecutionLane(childExecutionLane(nestedDepth));
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工作流实例启动时使用的定义快照。
|
||||
*
|
||||
* @param state 工作流状态
|
||||
* @return 活跃定义快照;当前实例首次由本节点接管时从仓储加载
|
||||
*/
|
||||
private ChainDefinition getDefinitionForInstance(ChainState state) {
|
||||
String stateInstanceId = state.getInstanceId();
|
||||
ChainDefinition definition = activeDefinitions.get(stateInstanceId);
|
||||
if (definition != null) {
|
||||
return definition;
|
||||
}
|
||||
ChainDefinition loaded = definitionSnapshotRepository.load(stateInstanceId);
|
||||
if (loaded == null) {
|
||||
// 兼容升级前已经启动、尚未持久化定义快照的实例。
|
||||
loaded = definitionRepository.getChainDefinitionById(state.getChainDefinitionId());
|
||||
}
|
||||
if (loaded == null) {
|
||||
return null;
|
||||
}
|
||||
synchronized (activeDefinitions) {
|
||||
ChainDefinition existing = activeDefinitions.get(stateInstanceId);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
activeDefinitions.put(stateInstanceId, loaded);
|
||||
return loaded;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void accept(Trigger trigger, ExecutorService worker) {
|
||||
ChainState state = chainStateRepository.load(trigger.getStateInstanceId());
|
||||
if (state == null) {
|
||||
throw new ChainException("Chain state not found");
|
||||
// 状态已过期或被清理时,该触发器已经失去业务目标,直接确认避免无限热重放。
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
ChainDefinition definition = definitionRepository.getChainDefinitionById(state.getChainDefinitionId());
|
||||
ChainDefinition definition = getDefinitionForInstance(state);
|
||||
if (definition == null) {
|
||||
throw new ChainException("Chain definition not found");
|
||||
throw new NonRetryableTriggerException(
|
||||
"Chain definition not found: " + state.getChainDefinitionId());
|
||||
}
|
||||
|
||||
Chain chain = new Chain(definition, trigger.getStateInstanceId());
|
||||
chain.setTriggerScheduler(triggerScheduler);
|
||||
chain.setChainStateRepository(chainStateRepository);
|
||||
chain.setNodeStateRepository(nodeStateRepository);
|
||||
chain.setEventManager(eventManager);
|
||||
Chain chain = configureChain(
|
||||
definition, trigger.getStateInstanceId(), state);
|
||||
|
||||
String nodeId = trigger.getNodeId();
|
||||
if (nodeId == null) {
|
||||
throw new ChainException("Node ID not found in trigger.");
|
||||
throw new NonRetryableTriggerException("Node ID not found in trigger.");
|
||||
}
|
||||
|
||||
Node node = definition.getNodeById(nodeId);
|
||||
if (node == null) {
|
||||
throw new ChainException("Node not found in definition(id: " + definition.getId() + ")");
|
||||
throw new NonRetryableTriggerException(
|
||||
"Node not found in definition(id: " + definition.getId() + ")");
|
||||
}
|
||||
|
||||
chain.executeNode(node, trigger);
|
||||
@@ -345,6 +1064,16 @@ public class ChainExecutor {
|
||||
return triggerScheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在查询/API 边界透明还原循环结果引用。
|
||||
*
|
||||
* @param value 可能包含引用的值
|
||||
* @return 业务可见值
|
||||
*/
|
||||
public Object resolveResultReferences(Object value) {
|
||||
return loopResultRepository.resolveReferences(value);
|
||||
}
|
||||
|
||||
public EventManager getEventManager() {
|
||||
return eventManager;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
package com.easyagents.flow.core.chain.runtime;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 工作流执行的全局资源保护预算。
|
||||
* <p>
|
||||
* 所有默认值均为宽松的失控保护值。小于等于 {@code 0} 的配置表示关闭对应保护,
|
||||
* 节点自身的循环次数、退出条件和重试配置仍按原有语义优先生效。
|
||||
*/
|
||||
public final class ExecutionBudget implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
public static final long DEFAULT_MAX_ITERATIONS = 100_000L;
|
||||
/**
|
||||
* 缺省不限制墙钟时长,避免人工确认或长期挂起时间被误计为执行耗时。
|
||||
*/
|
||||
public static final long DEFAULT_MAX_DURATION_MILLIS = 0L;
|
||||
public static final long DEFAULT_MAX_CHILD_EXECUTIONS = 1_000_000L;
|
||||
public static final long DEFAULT_MAX_ACCUMULATED_BYTES = 512L * 1024L * 1024L;
|
||||
public static final int DEFAULT_MAX_NESTED_DEPTH = 32;
|
||||
/**
|
||||
* 热状态硬限制缺省关闭。循环历史已通过引用隔离,开启限制时由部署方按实际负载设置,
|
||||
* 避免估算误差改变既有业务语义。
|
||||
*/
|
||||
public static final long DEFAULT_MAX_HOT_STATE_BYTES = 0L;
|
||||
|
||||
private final long maxIterations;
|
||||
private final long maxDurationMillis;
|
||||
private final long maxChildExecutions;
|
||||
private final long maxAccumulatedBytes;
|
||||
private final int maxNestedDepth;
|
||||
private final long maxHotStateBytes;
|
||||
|
||||
/**
|
||||
* 创建执行预算。
|
||||
*
|
||||
* @param maxIterations 单循环最大迭代次数
|
||||
* @param maxDurationMillis 单实例最大运行毫秒数
|
||||
* @param maxChildExecutions 单实例最大节点执行次数
|
||||
* @param maxAccumulatedBytes 单循环最大累计结果字节数
|
||||
* @param maxNestedDepth 最大循环嵌套深度
|
||||
* @param maxHotStateBytes 单实例热状态建议最大字节数
|
||||
*/
|
||||
public ExecutionBudget(long maxIterations,
|
||||
long maxDurationMillis,
|
||||
long maxChildExecutions,
|
||||
long maxAccumulatedBytes,
|
||||
int maxNestedDepth,
|
||||
long maxHotStateBytes) {
|
||||
this.maxIterations = maxIterations;
|
||||
this.maxDurationMillis = maxDurationMillis;
|
||||
this.maxChildExecutions = maxChildExecutions;
|
||||
this.maxAccumulatedBytes = maxAccumulatedBytes;
|
||||
this.maxNestedDepth = maxNestedDepth;
|
||||
this.maxHotStateBytes = maxHotStateBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建使用宽松缺省值的执行预算。
|
||||
*
|
||||
* @return 默认执行预算
|
||||
*/
|
||||
public static ExecutionBudget defaults() {
|
||||
return new ExecutionBudget(
|
||||
DEFAULT_MAX_ITERATIONS,
|
||||
DEFAULT_MAX_DURATION_MILLIS,
|
||||
DEFAULT_MAX_CHILD_EXECUTIONS,
|
||||
DEFAULT_MAX_ACCUMULATED_BYTES,
|
||||
DEFAULT_MAX_NESTED_DEPTH,
|
||||
DEFAULT_MAX_HOT_STATE_BYTES);
|
||||
}
|
||||
|
||||
public long getMaxIterations() {
|
||||
return maxIterations;
|
||||
}
|
||||
|
||||
public long getMaxDurationMillis() {
|
||||
return maxDurationMillis;
|
||||
}
|
||||
|
||||
public long getMaxChildExecutions() {
|
||||
return maxChildExecutions;
|
||||
}
|
||||
|
||||
public long getMaxAccumulatedBytes() {
|
||||
return maxAccumulatedBytes;
|
||||
}
|
||||
|
||||
public int getMaxNestedDepth() {
|
||||
return maxNestedDepth;
|
||||
}
|
||||
|
||||
public long getMaxHotStateBytes() {
|
||||
return maxHotStateBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验循环迭代总数。
|
||||
*
|
||||
* @param nodeId 循环节点 ID
|
||||
* @param iterations 计划迭代次数
|
||||
* @throws ExecutionBudgetExceededException 超过启用的迭代预算时抛出
|
||||
*/
|
||||
public void checkIterations(String nodeId, long iterations) {
|
||||
if (maxIterations > 0 && iterations > maxIterations) {
|
||||
throw new ExecutionBudgetExceededException(
|
||||
"Loop iteration budget exceeded for node " + nodeId
|
||||
+ ": " + iterations + " > " + maxIterations);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验循环嵌套深度。
|
||||
*
|
||||
* @param nodeId 节点 ID
|
||||
* @param depth 当前深度
|
||||
* @throws ExecutionBudgetExceededException 超过启用的深度预算时抛出
|
||||
*/
|
||||
public void checkNestedDepth(String nodeId, int depth) {
|
||||
if (maxNestedDepth > 0 && depth > maxNestedDepth) {
|
||||
throw new ExecutionBudgetExceededException(
|
||||
"Loop nested depth budget exceeded for node " + nodeId
|
||||
+ ": " + depth + " > " + maxNestedDepth);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验循环累计结果大小。
|
||||
*
|
||||
* @param nodeId 循环节点 ID
|
||||
* @param accumulatedBytes 当前累计估算字节数
|
||||
* @throws ExecutionBudgetExceededException 超过启用的累计结果预算时抛出
|
||||
*/
|
||||
public void checkAccumulatedBytes(String nodeId, long accumulatedBytes) {
|
||||
if (maxAccumulatedBytes > 0 && accumulatedBytes > maxAccumulatedBytes) {
|
||||
throw new ExecutionBudgetExceededException(
|
||||
"Loop accumulated result budget exceeded for node " + nodeId
|
||||
+ ": " + accumulatedBytes + " > " + maxAccumulatedBytes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验单实例节点执行次数。
|
||||
*
|
||||
* @param executions 当前节点执行次数
|
||||
* @throws ExecutionBudgetExceededException 超过启用的执行预算时抛出
|
||||
*/
|
||||
public void checkChildExecutions(long executions) {
|
||||
if (maxChildExecutions > 0 && executions > maxChildExecutions) {
|
||||
throw new ExecutionBudgetExceededException(
|
||||
"Workflow child execution budget exceeded: "
|
||||
+ executions + " > " + maxChildExecutions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验单实例运行时长。
|
||||
*
|
||||
* @param startedAtMillis 实例开始时间
|
||||
* @param nowMillis 当前时间
|
||||
* @throws ExecutionBudgetExceededException 超过启用的时长预算时抛出
|
||||
*/
|
||||
public void checkDuration(long startedAtMillis, long nowMillis) {
|
||||
if (maxDurationMillis > 0
|
||||
&& startedAtMillis > 0
|
||||
&& nowMillis - startedAtMillis > maxDurationMillis) {
|
||||
throw new ExecutionBudgetExceededException(
|
||||
"Workflow duration budget exceeded: "
|
||||
+ (nowMillis - startedAtMillis) + "ms > " + maxDurationMillis + "ms");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验工作流热状态估算大小。
|
||||
*
|
||||
* @param estimatedBytes 当前热状态估算字节数
|
||||
* @throws ExecutionBudgetExceededException 超过启用的热状态预算时抛出
|
||||
*/
|
||||
public void checkHotStateBytes(long estimatedBytes) {
|
||||
if (maxHotStateBytes > 0 && estimatedBytes > maxHotStateBytes) {
|
||||
throw new ExecutionBudgetExceededException(
|
||||
"Workflow hot state budget exceeded: "
|
||||
+ estimatedBytes + " > " + maxHotStateBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.easyagents.flow.core.chain.runtime;
|
||||
|
||||
import com.easyagents.flow.core.chain.ChainException;
|
||||
|
||||
/**
|
||||
* 工作流实例超过平台资源保护预算时抛出的异常。
|
||||
*/
|
||||
public class ExecutionBudgetExceededException extends ChainException {
|
||||
|
||||
/**
|
||||
* 创建预算超限异常。
|
||||
*
|
||||
* @param message 可审计的超限原因
|
||||
*/
|
||||
public ExecutionBudgetExceededException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,16 @@ package com.easyagents.flow.core.chain.runtime;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public class InMemoryTriggerStore implements TriggerStore {
|
||||
|
||||
private final ConcurrentHashMap<String, Trigger> store = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, AtomicLong> fencingTokens = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public Trigger save(Trigger trigger) {
|
||||
@@ -34,6 +37,18 @@ public class InMemoryTriggerStore implements TriggerStore {
|
||||
return trigger;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean saveIfAbsent(Trigger trigger) {
|
||||
if (trigger.getId() == null || trigger.getId().isBlank()) {
|
||||
throw new IllegalArgumentException("Stable trigger ID required");
|
||||
}
|
||||
return store.putIfAbsent(
|
||||
trigger.getId(), trigger) == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(String triggerId) {
|
||||
return store.remove(triggerId) != null;
|
||||
@@ -46,12 +61,35 @@ public class InMemoryTriggerStore implements TriggerStore {
|
||||
|
||||
@Override
|
||||
public List<Trigger> findDue(long uptoTimestamp) {
|
||||
return null;
|
||||
List<Trigger> due = new ArrayList<>();
|
||||
for (Trigger trigger : store.values()) {
|
||||
if (trigger.getTriggerAt() <= uptoTimestamp) {
|
||||
due.add(trigger);
|
||||
}
|
||||
}
|
||||
due.sort(Comparator.comparingLong(Trigger::getTriggerAt));
|
||||
return due;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Trigger> findAllPending() {
|
||||
return new ArrayList<>(store.values());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Trigger claim(String triggerId, long leaseMillis) {
|
||||
Trigger trigger = store.remove(triggerId);
|
||||
if (trigger != null) {
|
||||
String fencingScope = trigger.getStateInstanceId() == null
|
||||
? "__trigger__:" + trigger.getId()
|
||||
: trigger.getStateInstanceId();
|
||||
trigger.setFencingToken(fencingTokens
|
||||
.computeIfAbsent(fencingScope, ignored -> new AtomicLong())
|
||||
.incrementAndGet());
|
||||
}
|
||||
return trigger;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.easyagents.flow.core.chain.runtime;
|
||||
|
||||
/**
|
||||
* 表示触发器内容已经无法继续执行,应进入死信而非无限重放。
|
||||
*/
|
||||
public class NonRetryableTriggerException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* 创建不可重试触发器异常。
|
||||
*
|
||||
* @param message 异常说明
|
||||
*/
|
||||
public NonRetryableTriggerException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.easyagents.flow.core.chain.runtime;
|
||||
|
||||
/**
|
||||
* 表示当前节点遇到短暂基础设施冲突,应重新投递同一触发器且不消耗业务重试次数。
|
||||
*/
|
||||
public class RetryableTriggerException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* 创建可重新投递异常。
|
||||
*
|
||||
* @param message 异常说明
|
||||
* @param cause 原始异常
|
||||
*/
|
||||
public RetryableTriggerException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -16,14 +16,65 @@
|
||||
package com.easyagents.flow.core.chain.runtime;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class Trigger implements Serializable {
|
||||
private static final long serialVersionUID = 3165037658498721088L;
|
||||
|
||||
private String id;
|
||||
private String stateInstanceId;
|
||||
private String edgeId;
|
||||
private String nodeId; // 可以为 null,代表触发整个 chain
|
||||
private TriggerType type;
|
||||
private long triggerAt; // epoch ms
|
||||
/**
|
||||
* 当前运行时分配的触发器认领代际。
|
||||
*
|
||||
* <p>字段名为兼容既有序列化数据保留。分布式仓储在触发器认领成功时分配,
|
||||
* 并与该触发器租约共同续期和失效;该值不代表实例锁 fencing token。</p>
|
||||
*/
|
||||
private long fencingToken;
|
||||
/**
|
||||
* 创建派生触发器时必须仍然有效的父触发器 fencing token。
|
||||
*/
|
||||
private long requiredFencingToken;
|
||||
/**
|
||||
* 创建派生触发器时必须仍然有效的父实例锁 fencing token。
|
||||
*/
|
||||
private long requiredLockFencingToken;
|
||||
/**
|
||||
* 创建派生触发器时必须仍然有效的父触发器 claim ID。
|
||||
*/
|
||||
private String requiredFencingClaimId;
|
||||
/**
|
||||
* 基础设施投递失败次数,不占用业务节点重试次数。
|
||||
*/
|
||||
private int deliveryAttempt;
|
||||
/**
|
||||
* 已完成业务终态收敛、等待可靠写入死信的标记。
|
||||
*/
|
||||
private boolean deadLetterPending;
|
||||
/**
|
||||
* 待写入死信的稳定失败原因。
|
||||
*/
|
||||
private String deadLetterReason;
|
||||
/**
|
||||
* 跨重试保持不变的逻辑执行 ID,用于副作用幂等键。
|
||||
*/
|
||||
private String logicalExecutionId;
|
||||
/**
|
||||
* 可选执行通道,用于把会同步等待的子工作流与普通节点工作线程隔离。
|
||||
*/
|
||||
private String executionLane;
|
||||
/**
|
||||
* 首个稳定入口意图携带的初始变量。
|
||||
*
|
||||
* <p>仅用于实例仍处于 READY 时的崩溃恢复;正常启动提交后,运行时变量仍以
|
||||
* {@code ChainState.memory} 为唯一业务数据源。</p>
|
||||
*/
|
||||
private Map<String, Object> startVariables;
|
||||
private Map<String, LoopCursor> loopCursors;
|
||||
|
||||
public Trigger() {
|
||||
}
|
||||
@@ -77,6 +128,260 @@ public class Trigger implements Serializable {
|
||||
this.triggerAt = triggerAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前节点逻辑执行 ID。
|
||||
*
|
||||
* @return 跨重试保持不变的逻辑执行 ID
|
||||
*/
|
||||
public String getLogicalExecutionId() {
|
||||
return logicalExecutionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前节点逻辑执行 ID。
|
||||
*
|
||||
* @param logicalExecutionId 跨重试保持不变的逻辑执行 ID
|
||||
*/
|
||||
public void setLogicalExecutionId(String logicalExecutionId) {
|
||||
this.logicalExecutionId = logicalExecutionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取执行通道。
|
||||
*
|
||||
* @return 通道名;{@code null} 表示默认通道
|
||||
*/
|
||||
public String getExecutionLane() {
|
||||
return executionLane;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置执行通道。
|
||||
*
|
||||
* @param executionLane 通道名
|
||||
*/
|
||||
public void setExecutionLane(String executionLane) {
|
||||
this.executionLane = executionLane;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取崩溃恢复所需的初始变量。
|
||||
*
|
||||
* @return 初始变量快照;未携带时为 {@code null}
|
||||
*/
|
||||
public Map<String, Object> getStartVariables() {
|
||||
return startVariables;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置崩溃恢复所需的初始变量。
|
||||
*
|
||||
* @param startVariables 初始变量;仅首个稳定入口意图需要携带
|
||||
*/
|
||||
public void setStartVariables(Map<String, Object> startVariables) {
|
||||
this.startVariables = startVariables == null
|
||||
? null
|
||||
: new LinkedHashMap<>(startVariables);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本次认领代际。
|
||||
*
|
||||
* @return 单触发器认领代际;未认领时为 {@code 0}
|
||||
*/
|
||||
public long getFencingToken() {
|
||||
return fencingToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置本次认领代际。
|
||||
*
|
||||
* @param fencingToken 单触发器认领代际
|
||||
*/
|
||||
public void setFencingToken(long fencingToken) {
|
||||
this.fencingToken = fencingToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取保存派生触发器所依赖的父实例锁 fencing token。
|
||||
*
|
||||
* @return 父实例锁 token;无锁约束时为 {@code 0}
|
||||
*/
|
||||
public long getRequiredLockFencingToken() {
|
||||
return requiredLockFencingToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置保存派生触发器所依赖的父实例锁 fencing token。
|
||||
*
|
||||
* @param requiredLockFencingToken 父实例锁 token
|
||||
*/
|
||||
public void setRequiredLockFencingToken(long requiredLockFencingToken) {
|
||||
this.requiredLockFencingToken = requiredLockFencingToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取保存派生触发器所依赖的父 fencing token。
|
||||
*
|
||||
* @return 父 fencing token;无父认领约束时为 {@code 0}
|
||||
*/
|
||||
public long getRequiredFencingToken() {
|
||||
return requiredFencingToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置保存派生触发器所依赖的父 fencing token。
|
||||
*
|
||||
* @param requiredFencingToken 父 fencing token
|
||||
*/
|
||||
public void setRequiredFencingToken(long requiredFencingToken) {
|
||||
this.requiredFencingToken = requiredFencingToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取保存派生触发器所依赖的父 claim ID。
|
||||
*
|
||||
* @return 父触发器 ID;无父认领约束时为 {@code null}
|
||||
*/
|
||||
public String getRequiredFencingClaimId() {
|
||||
return requiredFencingClaimId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置保存派生触发器所依赖的父 claim ID。
|
||||
*
|
||||
* @param requiredFencingClaimId 父触发器 ID
|
||||
*/
|
||||
public void setRequiredFencingClaimId(String requiredFencingClaimId) {
|
||||
this.requiredFencingClaimId = requiredFencingClaimId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取基础设施投递失败次数。
|
||||
*
|
||||
* @return 失败次数
|
||||
*/
|
||||
public int getDeliveryAttempt() {
|
||||
return deliveryAttempt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置基础设施投递失败次数。
|
||||
*
|
||||
* @param deliveryAttempt 失败次数
|
||||
*/
|
||||
public void setDeliveryAttempt(int deliveryAttempt) {
|
||||
this.deliveryAttempt = deliveryAttempt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断触发器是否正在补写死信终态。
|
||||
*
|
||||
* @return 等待死信持久化时为 {@code true}
|
||||
*/
|
||||
public boolean isDeadLetterPending() {
|
||||
return deadLetterPending;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置死信补写标记。
|
||||
*
|
||||
* @param deadLetterPending 是否等待死信持久化
|
||||
*/
|
||||
public void setDeadLetterPending(boolean deadLetterPending) {
|
||||
this.deadLetterPending = deadLetterPending;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取稳定死信原因。
|
||||
*
|
||||
* @return 死信原因
|
||||
*/
|
||||
public String getDeadLetterReason() {
|
||||
return deadLetterReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置稳定死信原因。
|
||||
*
|
||||
* @param deadLetterReason 死信原因
|
||||
*/
|
||||
public void setDeadLetterReason(String deadLetterReason) {
|
||||
this.deadLetterReason = deadLetterReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取触发器携带的循环代际游标。
|
||||
*
|
||||
* @return 循环节点 ID 到游标的映射
|
||||
*/
|
||||
public Map<String, LoopCursor> getLoopCursors() {
|
||||
if (loopCursors == null) {
|
||||
loopCursors = new LinkedHashMap<>();
|
||||
}
|
||||
return loopCursors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置循环代际游标。
|
||||
*
|
||||
* @param loopCursors 循环节点 ID 到游标的映射
|
||||
*/
|
||||
public void setLoopCursors(Map<String, LoopCursor> loopCursors) {
|
||||
this.loopCursors = loopCursors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 循环分支代际游标,用于拒绝过期或重复的父节点回调。
|
||||
*/
|
||||
public static class LoopCursor implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String resultId;
|
||||
private int iterationIndex;
|
||||
private String branchId;
|
||||
|
||||
public LoopCursor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建循环游标。
|
||||
*
|
||||
* @param resultId 循环代际 ID
|
||||
* @param iterationIndex 迭代序号
|
||||
* @param branchId 直属分支 ID
|
||||
*/
|
||||
public LoopCursor(String resultId, int iterationIndex, String branchId) {
|
||||
this.resultId = resultId;
|
||||
this.iterationIndex = iterationIndex;
|
||||
this.branchId = branchId;
|
||||
}
|
||||
|
||||
public String getResultId() {
|
||||
return resultId;
|
||||
}
|
||||
|
||||
public void setResultId(String resultId) {
|
||||
this.resultId = resultId;
|
||||
}
|
||||
|
||||
public int getIterationIndex() {
|
||||
return iterationIndex;
|
||||
}
|
||||
|
||||
public void setIterationIndex(int iterationIndex) {
|
||||
this.iterationIndex = iterationIndex;
|
||||
}
|
||||
|
||||
public String getBranchId() {
|
||||
return branchId;
|
||||
}
|
||||
|
||||
public void setBranchId(String branchId) {
|
||||
this.branchId = branchId;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Trigger{" +
|
||||
@@ -89,4 +394,3 @@ public class Trigger implements Serializable {
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* Licensed under the GNU Lesser General Public License (LGPL), Version 3.0.
|
||||
*/
|
||||
package com.easyagents.flow.core.chain.runtime;
|
||||
|
||||
/**
|
||||
* 表示当前工作线程已经失去触发器租约,不再允许提交执行结果。
|
||||
*/
|
||||
public class TriggerClaimLostException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* 创建租约丢失异常。
|
||||
*
|
||||
* @param triggerId 已失去租约的触发器 ID
|
||||
*/
|
||||
public TriggerClaimLostException(String triggerId) {
|
||||
super("Workflow trigger claim ownership lost: " + triggerId);
|
||||
}
|
||||
}
|
||||
@@ -39,19 +39,49 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
public class TriggerScheduler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TriggerScheduler.class);
|
||||
private static final long CLAIM_LEASE_MS = TimeUnit.MINUTES.toMillis(1);
|
||||
private static final long CLAIM_RENEW_INTERVAL_MS = TimeUnit.SECONDS.toMillis(20);
|
||||
private static final int MAX_DELIVERY_ATTEMPTS = 20;
|
||||
private static final long MAX_REDELIVERY_DELAY_MS = TimeUnit.MINUTES.toMillis(1);
|
||||
/**
|
||||
* 本地仅缓存一批定时任务;容量溢出时持久仓储仍是恢复与补偿来源。
|
||||
*/
|
||||
private static final int MAX_LOCAL_SCHEDULED = 1024;
|
||||
/**
|
||||
* 精确定时只预热宽松近期限窗口,远期任务继续由持久仓储保管。
|
||||
*/
|
||||
private static final long MIN_LOCAL_SCHEDULE_HORIZON_MS =
|
||||
TimeUnit.MINUTES.toMillis(1);
|
||||
private final TriggerStore store;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final ExecutorService worker;
|
||||
private final Semaphore dispatchPermits;
|
||||
private final ConcurrentMap<String, ExecutorService> laneWorkers =
|
||||
new ConcurrentHashMap<>();
|
||||
private final ConcurrentMap<String, Semaphore> laneDispatchPermits =
|
||||
new ConcurrentHashMap<>();
|
||||
private final AtomicBoolean closed = new AtomicBoolean(false);
|
||||
|
||||
// map 用于管理取消:triggerId -> ScheduledFuture
|
||||
private final ConcurrentMap<String, ScheduledFuture<?>> scheduledFutures = new ConcurrentHashMap<>();
|
||||
/**
|
||||
* 本地 Future 对应的绝对触发时间,用于容量满时保留更早到期任务。
|
||||
*/
|
||||
private final ConcurrentMap<String, Long> scheduledTriggerTimes =
|
||||
new ConcurrentHashMap<>();
|
||||
private final ConcurrentMap<Trigger, ScheduledFuture<?>> claimRenewals = new ConcurrentHashMap<>();
|
||||
/**
|
||||
* 串行化本地 Future 的容量检查与登记,确保并发调度时仍严格受容量上限约束。
|
||||
*/
|
||||
private final Object localScheduleMonitor = new Object();
|
||||
|
||||
// consumer 来把 trigger 交给 ChainExecutor(或 ChainRuntime)去处理
|
||||
private volatile TriggerConsumer consumer;
|
||||
private volatile TriggerFailureListener failureListener;
|
||||
|
||||
// 周期扫查间隔(ms)
|
||||
private final long scanIntervalMs;
|
||||
private final long localScheduleHorizonMs;
|
||||
|
||||
// 扫描任务 future
|
||||
private ScheduledFuture<?> scanFuture;
|
||||
@@ -60,13 +90,34 @@ public class TriggerScheduler {
|
||||
void accept(Trigger trigger, ExecutorService worker);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发器不可恢复失败监听器。
|
||||
*/
|
||||
public interface TriggerFailureListener {
|
||||
/**
|
||||
* 在触发器 claim 仍有效时通知业务运行时收敛实例终态。
|
||||
*
|
||||
* @param trigger 已死信触发器
|
||||
* @param failure 最后一次执行失败
|
||||
*/
|
||||
boolean onDeadLetter(Trigger trigger, Throwable failure);
|
||||
}
|
||||
|
||||
public TriggerScheduler(TriggerStore store, ScheduledExecutorService scheduler, ExecutorService worker, long scanIntervalMs) {
|
||||
this.store = Objects.requireNonNull(store, "TriggerStore required");
|
||||
this.scheduler = Objects.requireNonNull(scheduler, "ScheduledExecutorService required");
|
||||
this.worker = Objects.requireNonNull(worker, "ExecutorService required");
|
||||
this.dispatchPermits = createDispatchPermits(worker);
|
||||
this.scanIntervalMs = Math.max(1000, scanIntervalMs);
|
||||
long scanHorizon = this.scanIntervalMs
|
||||
> Long.MAX_VALUE / 3L
|
||||
? Long.MAX_VALUE
|
||||
: this.scanIntervalMs * 3L;
|
||||
this.localScheduleHorizonMs = Math.max(
|
||||
MIN_LOCAL_SCHEDULE_HORIZON_MS,
|
||||
scanHorizon);
|
||||
|
||||
// 恢复并 schedule
|
||||
// 启动时只恢复已经到期的任务,避免把全部远期任务复制到本机 DelayQueue。
|
||||
recoverAndSchedulePending();
|
||||
|
||||
// 启动周期扫查 findDue
|
||||
@@ -78,6 +129,40 @@ public class TriggerScheduler {
|
||||
this.consumer = consumer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册触发器不可恢复失败监听器。
|
||||
*
|
||||
* @param listener 失败监听器
|
||||
*/
|
||||
public void registerFailureListener(TriggerFailureListener listener) {
|
||||
this.failureListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册独立执行通道。
|
||||
*
|
||||
* @param lane 通道名
|
||||
* @param laneWorker 通道工作线程池
|
||||
*/
|
||||
public void registerWorker(
|
||||
String lane, ExecutorService laneWorker) {
|
||||
if (lane == null || lane.isBlank()) {
|
||||
throw new IllegalArgumentException("lane required");
|
||||
}
|
||||
ExecutorService workerToRegister =
|
||||
Objects.requireNonNull(laneWorker, "laneWorker required");
|
||||
ExecutorService previous =
|
||||
laneWorkers.putIfAbsent(lane, workerToRegister);
|
||||
if (previous != null && previous != workerToRegister) {
|
||||
throw new IllegalStateException(
|
||||
"Trigger worker lane already registered: " + lane);
|
||||
}
|
||||
Semaphore permits = createDispatchPermits(workerToRegister);
|
||||
if (permits != null) {
|
||||
laneDispatchPermits.putIfAbsent(lane, permits);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* schedule a trigger: persist -> schedule (单机语义)
|
||||
*/
|
||||
@@ -86,20 +171,55 @@ public class TriggerScheduler {
|
||||
if (trigger.getId() == null) {
|
||||
trigger.setId(UUID.randomUUID().toString());
|
||||
}
|
||||
if (trigger.getLogicalExecutionId() == null || trigger.getLogicalExecutionId().isBlank()) {
|
||||
trigger.setLogicalExecutionId(trigger.getId());
|
||||
}
|
||||
store.save(trigger);
|
||||
scheduleInternal(trigger);
|
||||
return trigger;
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅在持久仓储中不存在同 ID 触发器时保存并调度。
|
||||
*
|
||||
* <p>调用方需持有工作流实例锁;该方法用于可重放启动协议,稳定 ID 可避免
|
||||
* READY 到入口触发器持久化之间的崩溃窗口产生重复待执行任务。</p>
|
||||
*
|
||||
* @param trigger 带稳定 ID 的触发器
|
||||
* @return 已存在或新保存的触发器
|
||||
*/
|
||||
public Trigger scheduleIfAbsent(Trigger trigger) {
|
||||
if (closed.get()) {
|
||||
throw new IllegalStateException("TriggerScheduler closed");
|
||||
}
|
||||
if (trigger == null || trigger.getId() == null
|
||||
|| trigger.getId().isBlank()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Stable trigger ID required");
|
||||
}
|
||||
if (trigger.getLogicalExecutionId() == null
|
||||
|| trigger.getLogicalExecutionId().isBlank()) {
|
||||
trigger.setLogicalExecutionId(
|
||||
trigger.getId());
|
||||
}
|
||||
if (store.saveIfAbsent(trigger)) {
|
||||
scheduleInternal(trigger);
|
||||
return trigger;
|
||||
}
|
||||
Trigger existing = store.find(
|
||||
trigger.getId());
|
||||
// 已有触发器可能刚好被其他 owner 认领;稳定入口仍视为已成功登记。
|
||||
return existing == null
|
||||
? trigger
|
||||
: existing;
|
||||
}
|
||||
|
||||
/**
|
||||
* cancel trigger (从 store 删除并尝试取消已 schedule 的 future)
|
||||
*/
|
||||
public boolean cancel(String triggerId) {
|
||||
boolean removed = store.remove(triggerId);
|
||||
ScheduledFuture<?> f = scheduledFutures.remove(triggerId);
|
||||
if (f != null) {
|
||||
f.cancel(false);
|
||||
}
|
||||
removeLocalSchedule(triggerId, true);
|
||||
return removed;
|
||||
}
|
||||
|
||||
@@ -107,83 +227,185 @@ public class TriggerScheduler {
|
||||
* 主动触发(webhook/event/manual 场景)
|
||||
*/
|
||||
public boolean fire(String triggerId) {
|
||||
if (closed.get()) return false;
|
||||
Trigger t = store.find(triggerId);
|
||||
if (t == null) return false;
|
||||
if (consumer == null) {
|
||||
// 无 consumer,仍从 store 中移除
|
||||
store.remove(triggerId);
|
||||
if (closed.get()) {
|
||||
return false;
|
||||
}
|
||||
// 在 worker 线程触发 consumer
|
||||
worker.submit(() -> {
|
||||
try {
|
||||
consumer.accept(t, worker);
|
||||
} catch (Exception e) {
|
||||
log.error(e.toString(), e);
|
||||
} finally {
|
||||
// 默认语义:触发后移除
|
||||
store.remove(triggerId);
|
||||
ScheduledFuture<?> sf = scheduledFutures.remove(triggerId);
|
||||
if (sf != null) sf.cancel(false);
|
||||
Trigger candidate = store.find(triggerId);
|
||||
if (candidate == null) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return true;
|
||||
removeLocalSchedule(triggerId, true);
|
||||
return claimAndDispatch(candidate);
|
||||
}
|
||||
|
||||
/**
|
||||
* internal scheduling for a trigger (单机 scheduled semantics)
|
||||
*/
|
||||
private void scheduleInternal(Trigger trigger) {
|
||||
if (closed.get()) return;
|
||||
|
||||
long delay = Math.max(0, trigger.getTriggerAt() - System.currentTimeMillis());
|
||||
|
||||
// cancel any existing scheduled future for same id
|
||||
ScheduledFuture<?> prev = scheduledFutures.remove(trigger.getId());
|
||||
if (prev != null) prev.cancel(false);
|
||||
|
||||
ScheduledFuture<?> future = scheduler.schedule(() -> {
|
||||
// double-check existence in store (可能已被 cancel)
|
||||
Trigger existing = store.find(trigger.getId());
|
||||
if (existing == null) {
|
||||
scheduledFutures.remove(trigger.getId());
|
||||
if (closed.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (consumer != null) {
|
||||
worker.submit(() -> {
|
||||
try {
|
||||
TriggerContext.setCurrentTrigger(existing);
|
||||
consumer.accept(existing, worker);
|
||||
} catch (Throwable e) {
|
||||
log.error(e.toString(), e);
|
||||
} finally {
|
||||
TriggerContext.clearCurrentTrigger();
|
||||
store.remove(existing.getId());
|
||||
scheduledFutures.remove(existing.getId());
|
||||
long now = System.currentTimeMillis();
|
||||
if (trigger.getTriggerAt()
|
||||
> scheduleHorizonTimestamp(now)) {
|
||||
return;
|
||||
}
|
||||
synchronized (localScheduleMonitor) {
|
||||
if (closed.get()) {
|
||||
return;
|
||||
}
|
||||
ScheduledFuture<?> existing;
|
||||
while ((existing = scheduledFutures.get(trigger.getId())) != null) {
|
||||
if (!existing.isDone() && !existing.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
// 已完成或取消的占位必须先原子移除,否则 putIfAbsent 会永久阻断重新调度。
|
||||
if (!scheduledFutures.remove(trigger.getId(), existing)) {
|
||||
continue;
|
||||
}
|
||||
scheduledTriggerTimes.remove(
|
||||
trigger.getId());
|
||||
}
|
||||
if (!hasLocalScheduleCapacity(
|
||||
trigger.getTriggerAt())) {
|
||||
return;
|
||||
}
|
||||
long delayMillis = Math.max(
|
||||
0L,
|
||||
trigger.getTriggerAt()
|
||||
- System.currentTimeMillis());
|
||||
ScheduledFuture<?> future = scheduler.schedule(
|
||||
() -> {
|
||||
// 回调先在同一登记锁下同时移除两张索引,消除零延迟任务
|
||||
// 在 Future 与时间索引分步登记之间完成所造成的孤儿记录。
|
||||
removeLocalSchedule(
|
||||
trigger.getId(), false);
|
||||
claimAndDispatch(trigger);
|
||||
},
|
||||
delayMillis,
|
||||
TimeUnit.MILLISECONDS);
|
||||
ScheduledFuture<?> concurrent =
|
||||
scheduledFutures.putIfAbsent(
|
||||
trigger.getId(), future);
|
||||
if (concurrent != null) {
|
||||
future.cancel(false);
|
||||
return;
|
||||
}
|
||||
scheduledTriggerTimes.put(
|
||||
trigger.getId(),
|
||||
trigger.getTriggerAt());
|
||||
// 零延迟任务可能在 put 前完成;完成态二次清理避免残留无效 Future。
|
||||
if (future.isDone() || future.isCancelled()) {
|
||||
scheduledFutures.remove(
|
||||
trigger.getId(), future);
|
||||
scheduledTriggerTimes.remove(
|
||||
trigger.getId());
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 无 consumer,则移除
|
||||
store.remove(existing.getId());
|
||||
scheduledFutures.remove(existing.getId());
|
||||
}
|
||||
}, delay, TimeUnit.MILLISECONDS);
|
||||
|
||||
scheduledFutures.put(trigger.getId(), future);
|
||||
/**
|
||||
* 检查本地调度容量,并在容量耗尽时清理已完成或已取消的占位。
|
||||
*
|
||||
* <p>正常路径只执行常量时间判断;达到上限时最多扫描
|
||||
* {@link #MAX_LOCAL_SCHEDULED} 个条目,避免极窄竞态残留导致永久停摆。</p>
|
||||
*
|
||||
* @param triggerAt 待登记任务的绝对触发时间
|
||||
* @return 仍可接收本地到期任务时为 {@code true}
|
||||
*/
|
||||
private boolean hasLocalScheduleCapacity(
|
||||
long triggerAt) {
|
||||
if (scheduledFutures.size() < MAX_LOCAL_SCHEDULED) {
|
||||
return true;
|
||||
}
|
||||
for (Map.Entry<String, ScheduledFuture<?>> entry : scheduledFutures.entrySet()) {
|
||||
ScheduledFuture<?> future = entry.getValue();
|
||||
if (future.isDone() || future.isCancelled()) {
|
||||
if (scheduledFutures.remove(
|
||||
entry.getKey(), future)) {
|
||||
scheduledTriggerTimes.remove(
|
||||
entry.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (scheduledFutures.size()
|
||||
< MAX_LOCAL_SCHEDULED) {
|
||||
return true;
|
||||
}
|
||||
Map.Entry<String, Long> latest = null;
|
||||
for (Map.Entry<String, Long> entry
|
||||
: scheduledTriggerTimes.entrySet()) {
|
||||
if (latest == null
|
||||
|| entry.getValue()
|
||||
> latest.getValue()) {
|
||||
latest = entry;
|
||||
}
|
||||
}
|
||||
if (latest == null
|
||||
|| latest.getValue() <= triggerAt) {
|
||||
return false;
|
||||
}
|
||||
removeLocalSchedule(
|
||||
latest.getKey(), true);
|
||||
return scheduledFutures.size()
|
||||
< MAX_LOCAL_SCHEDULED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算本轮应预热到本机的最远触发时间。
|
||||
*
|
||||
* @return 当前时间加本地近期限窗口;溢出时为最大时间戳
|
||||
*/
|
||||
private long scheduleHorizonTimestamp() {
|
||||
return scheduleHorizonTimestamp(
|
||||
System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于给定时间计算本机预热边界。
|
||||
*
|
||||
* @param now 当前时间戳
|
||||
* @return 当前时间加近期限窗口;溢出时为最大时间戳
|
||||
*/
|
||||
private long scheduleHorizonTimestamp(
|
||||
long now) {
|
||||
return now > Long.MAX_VALUE
|
||||
- localScheduleHorizonMs
|
||||
? Long.MAX_VALUE
|
||||
: now + localScheduleHorizonMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同时移除本地 Future 与其触发时间索引。
|
||||
*
|
||||
* @param triggerId 触发器 ID
|
||||
* @param cancel 是否取消尚未完成的 Future
|
||||
* @return 被移除的 Future;不存在时为 {@code null}
|
||||
*/
|
||||
private ScheduledFuture<?> removeLocalSchedule(
|
||||
String triggerId,
|
||||
boolean cancel) {
|
||||
synchronized (localScheduleMonitor) {
|
||||
ScheduledFuture<?> scheduled =
|
||||
scheduledFutures.remove(triggerId);
|
||||
scheduledTriggerTimes.remove(triggerId);
|
||||
if (cancel && scheduled != null) {
|
||||
scheduled.cancel(false);
|
||||
}
|
||||
return scheduled;
|
||||
}
|
||||
}
|
||||
|
||||
private void recoverAndSchedulePending() {
|
||||
try {
|
||||
List<Trigger> list = store.findAllPending();
|
||||
List<Trigger> list = store.findDue(
|
||||
scheduleHorizonTimestamp());
|
||||
if (list == null || list.isEmpty()) return;
|
||||
for (Trigger t : list) {
|
||||
scheduleInternal(t);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
// 忽略单次恢复错误,继续运行
|
||||
t.printStackTrace();
|
||||
log.error("Failed to recover pending workflow triggers", t);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +413,7 @@ public class TriggerScheduler {
|
||||
if (closed.get()) return;
|
||||
scanFuture = scheduler.scheduleAtFixedRate(() -> {
|
||||
try {
|
||||
long upto = System.currentTimeMillis();
|
||||
long upto = scheduleHorizonTimestamp();
|
||||
List<Trigger> due = store.findDue(upto);
|
||||
if (due == null || due.isEmpty()) return;
|
||||
for (Trigger t : due) {
|
||||
@@ -200,27 +422,363 @@ public class TriggerScheduler {
|
||||
if (sf != null && !sf.isDone() && !sf.isCancelled()) {
|
||||
continue;
|
||||
}
|
||||
// 直接提交到 worker,让 consumer 处理;并从 store 中移除
|
||||
if (consumer != null) {
|
||||
worker.submit(() -> {
|
||||
try {
|
||||
consumer.accept(t, worker);
|
||||
} finally {
|
||||
store.remove(t.getId());
|
||||
ScheduledFuture<?> f2 = scheduledFutures.remove(t.getId());
|
||||
if (f2 != null) f2.cancel(false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
store.remove(t.getId());
|
||||
}
|
||||
scheduleInternal(t);
|
||||
}
|
||||
} catch (Throwable tt) {
|
||||
tt.printStackTrace();
|
||||
log.error("Failed to scan due workflow triggers", tt);
|
||||
}
|
||||
}, scanIntervalMs, scanIntervalMs, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子认领触发器并提交给工作线程。
|
||||
*
|
||||
* @param candidate 已加载的候选触发器
|
||||
* @return 成功认领并提交时为 true
|
||||
*/
|
||||
private boolean claimAndDispatch(Trigger candidate) {
|
||||
if (candidate == null || candidate.getId() == null) {
|
||||
return false;
|
||||
}
|
||||
String triggerId = candidate.getId();
|
||||
ExecutorService dispatchWorker = resolveWorker(candidate);
|
||||
Semaphore selectedPermits = resolveDispatchPermits(candidate);
|
||||
if (selectedPermits != null && !selectedPermits.tryAcquire()) {
|
||||
// 保留持久化触发器并清理本地占位,让后续扫描能够重新调度。
|
||||
removeLocalSchedule(triggerId, false);
|
||||
return false;
|
||||
}
|
||||
Trigger claimed;
|
||||
try {
|
||||
claimed = store.claim(candidate, CLAIM_LEASE_MS);
|
||||
} catch (RuntimeException | Error error) {
|
||||
removeLocalSchedule(triggerId, false);
|
||||
releaseDispatchPermit(selectedPermits);
|
||||
throw error;
|
||||
}
|
||||
if (claimed == null) {
|
||||
releaseDispatchPermit(selectedPermits);
|
||||
removeLocalSchedule(triggerId, false);
|
||||
return false;
|
||||
}
|
||||
TriggerConsumer currentConsumer = consumer;
|
||||
if (currentConsumer == null) {
|
||||
releaseClaimBestEffort(claimed, "consumer is unavailable");
|
||||
releaseDispatchPermit(selectedPermits);
|
||||
removeLocalSchedule(triggerId, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
ScheduledFuture<?> renewal;
|
||||
try {
|
||||
renewal = scheduler.scheduleAtFixedRate(
|
||||
() -> renewClaim(claimed),
|
||||
CLAIM_RENEW_INTERVAL_MS,
|
||||
CLAIM_RENEW_INTERVAL_MS,
|
||||
TimeUnit.MILLISECONDS);
|
||||
} catch (RejectedExecutionException error) {
|
||||
releaseClaimBestEffort(claimed, "claim renewal scheduling was rejected");
|
||||
releaseDispatchPermit(selectedPermits);
|
||||
removeLocalSchedule(triggerId, false);
|
||||
return false;
|
||||
}
|
||||
ScheduledFuture<?> existingRenewal = claimRenewals.put(claimed, renewal);
|
||||
if (existingRenewal != null) {
|
||||
existingRenewal.cancel(false);
|
||||
}
|
||||
|
||||
try {
|
||||
dispatchWorker.submit(() -> consumeClaimedTrigger(
|
||||
claimed,
|
||||
currentConsumer,
|
||||
dispatchWorker,
|
||||
selectedPermits));
|
||||
return true;
|
||||
} catch (RejectedExecutionException error) {
|
||||
cancelClaimRenewal(claimed);
|
||||
releaseClaimBestEffort(claimed, "worker submission was rejected");
|
||||
releaseDispatchPermit(selectedPermits);
|
||||
removeLocalSchedule(triggerId, false);
|
||||
log.error("Workflow trigger worker rejected task, triggerId={}", triggerId, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行已认领触发器,并按执行结果确认或释放。
|
||||
*
|
||||
* @param trigger 已认领触发器
|
||||
* @param currentConsumer 本次执行使用的消费者快照
|
||||
*/
|
||||
private void consumeClaimedTrigger(
|
||||
Trigger trigger,
|
||||
TriggerConsumer currentConsumer,
|
||||
ExecutorService dispatchWorker,
|
||||
Semaphore selectedPermits) {
|
||||
boolean succeeded = false;
|
||||
boolean deadLetter = false;
|
||||
boolean retryWithoutDeadLetter = false;
|
||||
boolean rescheduleReleasedTrigger = false;
|
||||
Throwable failure = null;
|
||||
try {
|
||||
TriggerContext.setCurrentTrigger(trigger);
|
||||
if (trigger.isDeadLetterPending()) {
|
||||
deadLetter = true;
|
||||
failure = new NonRetryableTriggerException(
|
||||
trigger.getDeadLetterReason());
|
||||
} else {
|
||||
currentConsumer.accept(trigger, dispatchWorker);
|
||||
succeeded = true;
|
||||
}
|
||||
} catch (NonRetryableTriggerException error) {
|
||||
failure = error;
|
||||
deadLetter = true;
|
||||
log.error("Workflow trigger is not retryable, triggerId={}", trigger.getId(), error);
|
||||
} catch (RetryableTriggerException error) {
|
||||
failure = error;
|
||||
retryWithoutDeadLetter = true;
|
||||
log.warn(
|
||||
"Workflow trigger hit transient infrastructure contention, triggerId={}",
|
||||
trigger.getId());
|
||||
} catch (Throwable error) {
|
||||
failure = error;
|
||||
log.error("Workflow trigger execution failed, triggerId={}", trigger.getId(), error);
|
||||
} finally {
|
||||
try {
|
||||
if (succeeded) {
|
||||
store.acknowledge(trigger);
|
||||
} else {
|
||||
int deliveryAttempt = trigger.isDeadLetterPending()
|
||||
? Math.max(1, trigger.getDeliveryAttempt())
|
||||
: incrementDeliveryAttempt(trigger);
|
||||
if (retryWithoutDeadLetter) {
|
||||
trigger.setTriggerAt(
|
||||
System.currentTimeMillis()
|
||||
+ redeliveryDelayMillis(
|
||||
deliveryAttempt));
|
||||
store.release(trigger);
|
||||
rescheduleReleasedTrigger = true;
|
||||
} else if (deadLetter
|
||||
|| deliveryAttempt
|
||||
>= MAX_DELIVERY_ATTEMPTS) {
|
||||
String reason = failure == null
|
||||
? "delivery attempts exhausted"
|
||||
: failure.getClass().getName()
|
||||
+ ": "
|
||||
+ failure.getMessage();
|
||||
if (trigger.isDeadLetterPending()
|
||||
&& trigger.getDeadLetterReason() != null) {
|
||||
reason = trigger.getDeadLetterReason();
|
||||
} else {
|
||||
trigger.setDeadLetterPending(true);
|
||||
trigger.setDeadLetterReason(reason);
|
||||
store.markDeadLetterPending(
|
||||
trigger);
|
||||
}
|
||||
if (!store.renewClaim(
|
||||
trigger, CLAIM_LEASE_MS)) {
|
||||
throw new TriggerClaimLostException(
|
||||
trigger.getId());
|
||||
}
|
||||
TriggerFailureListener currentFailureListener =
|
||||
failureListener;
|
||||
if (currentFailureListener != null
|
||||
&& !currentFailureListener.onDeadLetter(
|
||||
trigger, failure)) {
|
||||
trigger.setTriggerAt(
|
||||
System.currentTimeMillis()
|
||||
+ MAX_REDELIVERY_DELAY_MS);
|
||||
store.release(trigger);
|
||||
rescheduleReleasedTrigger = true;
|
||||
return;
|
||||
}
|
||||
store.deadLetter(trigger, reason);
|
||||
} else {
|
||||
trigger.setTriggerAt(
|
||||
System.currentTimeMillis()
|
||||
+ redeliveryDelayMillis(
|
||||
deliveryAttempt));
|
||||
store.release(trigger);
|
||||
rescheduleReleasedTrigger = true;
|
||||
}
|
||||
}
|
||||
} catch (Throwable terminalStoreError) {
|
||||
if (trigger.isDeadLetterPending()) {
|
||||
releaseDeadLetterPendingBestEffort(trigger);
|
||||
}
|
||||
log.error(
|
||||
"Failed to finalize workflow trigger, triggerId={}",
|
||||
trigger.getId(),
|
||||
terminalStoreError);
|
||||
} finally {
|
||||
TriggerContext.clearCurrentTrigger();
|
||||
cancelClaimRenewal(trigger);
|
||||
removeLocalSchedule(
|
||||
trigger.getId(), false);
|
||||
releaseDispatchPermit(selectedPermits);
|
||||
if (rescheduleReleasedTrigger) {
|
||||
scheduleInternal(trigger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在仍持有 claim 时保存待补写死信标记,避免业务终态成功后被普通 ACK 吞掉。
|
||||
*
|
||||
* @param trigger 待补写死信的触发器
|
||||
*/
|
||||
private void releaseDeadLetterPendingBestEffort(
|
||||
Trigger trigger) {
|
||||
try {
|
||||
if (!store.renewClaim(trigger, CLAIM_LEASE_MS)) {
|
||||
return;
|
||||
}
|
||||
trigger.setTriggerAt(
|
||||
System.currentTimeMillis()
|
||||
+ MAX_REDELIVERY_DELAY_MS);
|
||||
store.release(trigger);
|
||||
} catch (Throwable releaseError) {
|
||||
log.error(
|
||||
"Failed to persist pending dead-letter finalization, triggerId={}",
|
||||
trigger.getId(),
|
||||
releaseError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尽力释放已认领触发器。
|
||||
*
|
||||
* <p>释放失败时保留 Redis 中的触发器和租约,等待租约自然过期恢复。调用方仍可继续
|
||||
* 清理本地调度状态和归还容量许可。</p>
|
||||
*
|
||||
* @param trigger 已认领触发器
|
||||
* @param reason 释放原因
|
||||
*/
|
||||
private void releaseClaimBestEffort(Trigger trigger, String reason) {
|
||||
try {
|
||||
store.release(trigger);
|
||||
} catch (Throwable error) {
|
||||
log.error(
|
||||
"Failed to release workflow trigger, triggerId={}, reason={}",
|
||||
trigger == null ? null : trigger.getId(),
|
||||
reason,
|
||||
error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加基础设施投递失败次数。
|
||||
*
|
||||
* @param trigger 当前触发器
|
||||
* @return 增加后的次数
|
||||
*/
|
||||
private int incrementDeliveryAttempt(Trigger trigger) {
|
||||
int attempt = Math.max(0, trigger.getDeliveryAttempt()) + 1;
|
||||
trigger.setDeliveryAttempt(attempt);
|
||||
return attempt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算带上限的指数退避,避免缺失定义或短暂冲突形成热循环。
|
||||
*
|
||||
* @param attempt 已失败次数
|
||||
* @return 下次投递延迟毫秒数
|
||||
*/
|
||||
private long redeliveryDelayMillis(int attempt) {
|
||||
int shift = Math.min(16, Math.max(0, attempt - 1));
|
||||
long delay = 1_000L << shift;
|
||||
return Math.min(MAX_REDELIVERY_DELAY_MS, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据工作线程池的真实容量建立领取前背压。
|
||||
*
|
||||
* @param executor 工作线程池
|
||||
* @return 容量信号量;无法识别容量时返回 null
|
||||
*/
|
||||
private Semaphore createDispatchPermits(ExecutorService executor) {
|
||||
if (!(executor instanceof ThreadPoolExecutor)) {
|
||||
return null;
|
||||
}
|
||||
ThreadPoolExecutor pool = (ThreadPoolExecutor) executor;
|
||||
long capacity = (long) pool.getMaximumPoolSize() + pool.getQueue().remainingCapacity();
|
||||
return new Semaphore((int) Math.max(1L, Math.min(Integer.MAX_VALUE, capacity)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放一个工作线程容量许可。
|
||||
*/
|
||||
private void releaseDispatchPermit(Semaphore permits) {
|
||||
if (permits != null) {
|
||||
permits.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析触发器执行线程池。
|
||||
*
|
||||
* @param trigger 触发器
|
||||
* @return 默认或独立通道线程池
|
||||
*/
|
||||
private ExecutorService resolveWorker(Trigger trigger) {
|
||||
String lane = trigger == null ? null : trigger.getExecutionLane();
|
||||
return lane == null ? worker : laneWorkers.getOrDefault(lane, worker);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析所选线程池对应的领取前背压许可。
|
||||
*
|
||||
* @param trigger 触发器
|
||||
* @return 容量许可;无法识别时为 {@code null}
|
||||
*/
|
||||
private Semaphore resolveDispatchPermits(Trigger trigger) {
|
||||
String lane = trigger == null ? null : trigger.getExecutionLane();
|
||||
return lane == null
|
||||
? dispatchPermits
|
||||
: laneDispatchPermits.getOrDefault(lane, dispatchPermits);
|
||||
}
|
||||
|
||||
/**
|
||||
* 续期触发器认领租约。
|
||||
*
|
||||
* @param trigger 已认领触发器
|
||||
*/
|
||||
private void renewClaim(Trigger trigger) {
|
||||
try {
|
||||
if (!store.renewClaim(trigger, CLAIM_LEASE_MS)) {
|
||||
log.warn("Workflow trigger claim renewal lost ownership, triggerId={}", trigger.getId());
|
||||
cancelClaimRenewal(trigger);
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
log.error("Workflow trigger claim renewal failed, triggerId={}", trigger.getId(), error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在业务状态提交前验证具体触发器仍由当前工作线程持有。
|
||||
*
|
||||
* @param trigger 已认领触发器
|
||||
* @throws TriggerClaimLostException 租约已失效或已转移
|
||||
*/
|
||||
public void assertClaimOwned(Trigger trigger) {
|
||||
if (trigger != null && !store.renewClaim(trigger, CLAIM_LEASE_MS)) {
|
||||
throw new TriggerClaimLostException(trigger.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消触发器租约续期任务。
|
||||
*
|
||||
* @param trigger 已认领触发器
|
||||
*/
|
||||
private void cancelClaimRenewal(Trigger trigger) {
|
||||
ScheduledFuture<?> renewal = claimRenewals.remove(trigger);
|
||||
if (renewal != null) {
|
||||
renewal.cancel(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
if (closed.compareAndSet(false, true)) {
|
||||
if (scanFuture != null) scanFuture.cancel(false);
|
||||
@@ -232,6 +790,11 @@ public class TriggerScheduler {
|
||||
}
|
||||
}
|
||||
scheduledFutures.clear();
|
||||
scheduledTriggerTimes.clear();
|
||||
for (ScheduledFuture<?> renewal : claimRenewals.values()) {
|
||||
renewal.cancel(false);
|
||||
}
|
||||
claimRenewals.clear();
|
||||
|
||||
try {
|
||||
scheduler.shutdownNow();
|
||||
@@ -241,6 +804,18 @@ public class TriggerScheduler {
|
||||
worker.shutdownNow();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
for (ExecutorService laneWorker :
|
||||
new java.util.HashSet<>(laneWorkers.values())) {
|
||||
if (laneWorker == worker) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
laneWorker.shutdownNow();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
laneWorkers.clear();
|
||||
laneDispatchPermits.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,27 @@ package com.easyagents.flow.core.chain.runtime;
|
||||
import java.util.List;
|
||||
|
||||
public interface TriggerStore {
|
||||
|
||||
Trigger save(Trigger trigger);
|
||||
|
||||
/**
|
||||
* 仅在同 ID 触发器尚不存在时原子保存。
|
||||
*
|
||||
* <p>缺省实现保证同一仓储实例内原子;分布式仓储必须覆盖为跨进程原子操作。</p>
|
||||
*
|
||||
* @param trigger 带稳定 ID 的触发器
|
||||
* @return 本次成功创建时为 {@code true},已存在时为 {@code false}
|
||||
*/
|
||||
default boolean saveIfAbsent(Trigger trigger) {
|
||||
synchronized (this) {
|
||||
if (find(trigger.getId()) != null) {
|
||||
return false;
|
||||
}
|
||||
save(trigger);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
boolean remove(String triggerId);
|
||||
|
||||
Trigger find(String triggerId);
|
||||
@@ -27,4 +46,116 @@ public interface TriggerStore {
|
||||
List<Trigger> findDue(long uptoTimestamp);
|
||||
|
||||
List<Trigger> findAllPending();
|
||||
|
||||
/**
|
||||
* 原子认领待执行触发器。
|
||||
* <p>
|
||||
* 缺省实现适用于单进程仓储:先读取再以删除结果作为认领成功标志。
|
||||
*
|
||||
* @param triggerId 触发器 ID
|
||||
* @param leaseMillis 认领租约毫秒数
|
||||
* @return 认领成功时返回触发器,否则返回 null
|
||||
*/
|
||||
default Trigger claim(String triggerId, long leaseMillis) {
|
||||
Trigger trigger = find(triggerId);
|
||||
return trigger != null && remove(triggerId) ? trigger : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子认领已加载的待执行触发器。
|
||||
*
|
||||
* <p>分布式仓储可使用候选触发器中的实例 ID 构建与本次 claim 绑定的执行守卫,
|
||||
* 避免认领前额外读取完整触发器负载。</p>
|
||||
*
|
||||
* @param candidate 扫描或主动触发阶段已加载的候选触发器
|
||||
* @param leaseMillis 认领租约毫秒数
|
||||
* @return 认领成功时返回触发器,否则返回 {@code null}
|
||||
*/
|
||||
default Trigger claim(Trigger candidate, long leaseMillis) {
|
||||
return candidate == null ? null : claim(candidate.getId(), leaseMillis);
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅按 ID 续期触发器租约。
|
||||
*
|
||||
* <p>分布式仓储无法仅凭 ID 验证 owner token。该兼容入口不应再用于运行时提交路径,
|
||||
* 调用方必须保留 {@link Trigger} 认领对象并使用
|
||||
* {@link #renewClaim(Trigger, long)}。</p>
|
||||
*
|
||||
* @param triggerId 触发器 ID
|
||||
* @param leaseMillis 新租约毫秒数
|
||||
* @return 不适用
|
||||
* @throws UnsupportedOperationException 始终抛出,防止无 owner token 的不安全续期
|
||||
*/
|
||||
@Deprecated
|
||||
default boolean renewClaim(String triggerId, long leaseMillis) {
|
||||
throw new UnsupportedOperationException("Trigger claim token is required");
|
||||
}
|
||||
|
||||
/**
|
||||
* 续期调用方持有的具体触发器租约。
|
||||
*
|
||||
* @param trigger 已认领触发器对象
|
||||
* @param leaseMillis 新租约毫秒数
|
||||
* @return 续期成功时为 true
|
||||
*/
|
||||
default boolean renewClaim(Trigger trigger, long leaseMillis) {
|
||||
// 单进程缺省仓储在 claim 时已经移除触发器,不需要租约续期。
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅按 ID 确认触发器。
|
||||
*
|
||||
* <p>分布式仓储无法仅凭 ID 验证 owner token,运行时必须使用
|
||||
* {@link #acknowledge(Trigger)}。</p>
|
||||
*
|
||||
* @param triggerId 触发器 ID
|
||||
* @throws UnsupportedOperationException 始终抛出,防止旧 owner 删除新 owner 的任务
|
||||
*/
|
||||
@Deprecated
|
||||
default void acknowledge(String triggerId) {
|
||||
throw new UnsupportedOperationException("Claimed trigger object is required");
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认调用方持有的具体触发器执行成功。
|
||||
*
|
||||
* @param trigger 已认领触发器对象
|
||||
*/
|
||||
default void acknowledge(Trigger trigger) {
|
||||
// 单进程缺省认领已经移除触发器,无需再次处理。
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放失败执行的触发器,使其可以再次被认领。
|
||||
*
|
||||
* @param trigger 执行失败的触发器
|
||||
*/
|
||||
default void release(Trigger trigger) {
|
||||
save(trigger);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在保持当前 claim 的同时持久化待补写死信标记。
|
||||
*
|
||||
* <p>分布式仓储必须校验具体 owner token;进程崩溃后,新 owner 依靠该标记
|
||||
* 跳过业务执行并继续终态协议。</p>
|
||||
*
|
||||
* @param trigger 已认领且标记为待补写死信的触发器
|
||||
*/
|
||||
default void markDeadLetterPending(
|
||||
Trigger trigger) {
|
||||
// 单进程仓储的 claimed trigger 仅存在于当前调用栈,无需额外持久化。
|
||||
}
|
||||
|
||||
/**
|
||||
* 将不可继续执行或超过投递上限的触发器移入死信。
|
||||
*
|
||||
* @param trigger 已认领触发器
|
||||
* @param reason 死信原因
|
||||
*/
|
||||
default void deadLetter(Trigger trigger, String reason) {
|
||||
acknowledge(trigger);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
package com.easyagents.flow.core.code.impl;
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.ChainState;
|
||||
import com.easyagents.flow.core.chain.NodeState;
|
||||
import com.easyagents.flow.core.code.CodeRuntimeEngine;
|
||||
import com.easyagents.flow.core.node.CodeNode;
|
||||
import com.easyagents.flow.core.util.graalvm.JsInteropUtils;
|
||||
@@ -39,8 +41,13 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine {
|
||||
public Map<String, Object> execute(String code, CodeNode node, Chain chain) {
|
||||
try (Context context = CONTEXT_BUILDER.build()) {
|
||||
Value bindings = context.getBindings("js");
|
||||
ChainState chainState =
|
||||
chain.getExecutionState();
|
||||
NodeState nodeState =
|
||||
chain.getNodeState(node.getId());
|
||||
|
||||
Map<String, Object> all = chain.getState().getMemory();
|
||||
Map<String, Object> all =
|
||||
chainState.getMemory();
|
||||
all.forEach((key, value) -> {
|
||||
if (!key.contains(".")) {
|
||||
bindings.putMember(key, JsInteropUtils.wrapJavaValueForJS(context, value));
|
||||
@@ -48,23 +55,20 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine {
|
||||
});
|
||||
|
||||
// 注入参数
|
||||
Map<String, Object> parameterValues = chain.getState().resolveParameters(node);
|
||||
Map<String, Object> parameterValues =
|
||||
chainState.resolveParameters(node);
|
||||
if (parameterValues != null) {
|
||||
for (Map.Entry<String, Object> entry : parameterValues.entrySet()) {
|
||||
bindings.putMember(entry.getKey(), JsInteropUtils.wrapJavaValueForJS(context, entry.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
bindings.putMember("_chain", chain);
|
||||
bindings.putMember("_state", chain.getNodeState(node.getId()));
|
||||
|
||||
|
||||
// 在 JS 中创建 _result 对象
|
||||
context.eval("js", "var _result = {};");
|
||||
|
||||
// 注入 _chain 和 _context
|
||||
bindings.putMember("_chain", chain);
|
||||
bindings.putMember("_state", chain.getNodeState(node.getId()));
|
||||
bindings.putMember("_state", nodeState);
|
||||
|
||||
// 执行用户脚本
|
||||
context.eval("js", code);
|
||||
|
||||
@@ -85,6 +85,7 @@ public interface Llm {
|
||||
* 实现了Serializable接口,支持序列化
|
||||
*/
|
||||
class ChatOptions implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String seed;
|
||||
private Float temperature = 0.8f;
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class BaseNode extends Node {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
protected List<Parameter> parameters;
|
||||
protected List<Parameter> outputDefs;
|
||||
|
||||
@@ -27,6 +27,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class CodeNode extends BaseNode {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
protected String engine;
|
||||
protected String code;
|
||||
|
||||
@@ -52,7 +54,8 @@ public class CodeNode extends BaseNode {
|
||||
throw new IllegalArgumentException("code is empty");
|
||||
}
|
||||
|
||||
ChainState chainState = chain.getState();
|
||||
ChainState chainState =
|
||||
chain.getExecutionState();
|
||||
Map<String, Object> parameterValues = chainState.resolveParameters(this);
|
||||
String newCode = TextTemplate.of(code).formatToString(chainState.buildTemplateRootMaps(parameterValues));
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ import com.easyagents.flow.core.chain.repository.ChainStateField;
|
||||
import java.util.*;
|
||||
|
||||
public class ConfirmNode extends BaseNode {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
private String message;
|
||||
private List<Parameter> confirms;
|
||||
@@ -70,7 +72,8 @@ public class ConfirmNode extends BaseNode {
|
||||
|
||||
Map<String, Object> values;
|
||||
try {
|
||||
values = chain.getState().resolveParameters(this, confirmParameters);
|
||||
values = chain.getExecutionState()
|
||||
.resolveParameters(this, confirmParameters);
|
||||
// 移除 confirm 参数,方便在其他节点二次确认,或者在 for 循环中第二次获取
|
||||
chain.updateStateSafely(state -> {
|
||||
for (Parameter confirmParameter : confirmParameters) {
|
||||
@@ -94,7 +97,12 @@ public class ConfirmNode extends BaseNode {
|
||||
}
|
||||
|
||||
// 获取参数值,不会触发 ChainSuspendException 错误
|
||||
Map<String, Object> parameterValues = chain.getState().resolveParameters(this, newParameters, null, true);
|
||||
Map<String, Object> parameterValues =
|
||||
chain.getExecutionState().resolveParameters(
|
||||
this,
|
||||
newParameters,
|
||||
null,
|
||||
true);
|
||||
|
||||
// 设置 enums,方便前端给用户进行选择
|
||||
for (Parameter confirmParameter : confirmParameters) {
|
||||
|
||||
@@ -22,6 +22,8 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class EndNode extends BaseNode {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private boolean normal = true;
|
||||
private String message;
|
||||
|
||||
@@ -47,7 +49,8 @@ public class EndNode extends BaseNode {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
|
||||
ChainState chainState =
|
||||
chain.getExecutionState();
|
||||
Map<String, Object> output = new HashMap<>();
|
||||
if (normal) {
|
||||
output.put(ChainConsts.CHAIN_STATE_STATUS_KEY, ChainStatus.SUCCEEDED);
|
||||
@@ -62,7 +65,7 @@ public class EndNode extends BaseNode {
|
||||
if (this.outputDefs != null) {
|
||||
for (Parameter outputDef : this.outputDefs) {
|
||||
if (outputDef.getRefType() == RefType.REF) {
|
||||
output.put(outputDef.getName(), chain.getState().resolveValue(outputDef.getRef()));
|
||||
output.put(outputDef.getName(), chainState.resolveValue(outputDef.getRef()));
|
||||
} else if (outputDef.getRefType() == RefType.INPUT) {
|
||||
output.put(outputDef.getName(), outputDef.getRef());
|
||||
} else if (outputDef.getRefType() == RefType.FIXED) {
|
||||
@@ -70,7 +73,7 @@ public class EndNode extends BaseNode {
|
||||
}
|
||||
// default is ref type
|
||||
else if (StringUtil.hasText(outputDef.getRef())) {
|
||||
output.put(outputDef.getName(), chain.getState().resolveValue(outputDef.getRef()));
|
||||
output.put(outputDef.getName(), chainState.resolveValue(outputDef.getRef()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,21 +22,33 @@ import com.easyagents.flow.core.chain.DataType;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.filestoreage.FileStorage;
|
||||
import com.easyagents.flow.core.filestoreage.FileStorageManager;
|
||||
import com.easyagents.flow.core.util.IoBulkhead;
|
||||
import com.easyagents.flow.core.util.OkHttpClientUtil;
|
||||
import com.easyagents.flow.core.util.StringUtil;
|
||||
import com.easyagents.flow.core.util.TextTemplate;
|
||||
import okhttp3.*;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class HttpNode extends BaseNode {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
private static final long DEFAULT_MAX_TEXT_RESPONSE_BYTES = 64L * 1024L * 1024L;
|
||||
private static final long DEFAULT_MAX_FILE_RESPONSE_BYTES = 2L * 1024L * 1024L * 1024L;
|
||||
private static final String MAX_RESPONSE_BYTES_PROPERTY = "tinyflow.http.max-response-bytes";
|
||||
private static final String MAX_FILE_RESPONSE_BYTES_PROPERTY = "tinyflow.http.max-file-response-bytes";
|
||||
|
||||
private String url;
|
||||
private String method;
|
||||
@@ -145,7 +157,7 @@ public class HttpNode extends BaseNode {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
int maxRetry = 5;
|
||||
int maxRetry = supportsAutomaticRetry(method) ? 5 : 1;
|
||||
long retryInterval = 2000L;
|
||||
|
||||
int attempt = 0;
|
||||
@@ -161,7 +173,7 @@ public class HttpNode extends BaseNode {
|
||||
lastError = ex;
|
||||
|
||||
// 判断是否需要重试
|
||||
if (!shouldRetry(ex)) {
|
||||
if (attempt >= maxRetry || !shouldRetry(ex)) {
|
||||
throw wrapAsRuntime(ex, attempt);
|
||||
}
|
||||
|
||||
@@ -199,6 +211,19 @@ public class HttpNode extends BaseNode {
|
||||
return cause instanceof IOException;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前方法是否允许节点内部自动重试。
|
||||
*
|
||||
* @param requestMethod HTTP 方法
|
||||
* @return 仅无业务副作用的读取类方法返回 {@code true}
|
||||
*/
|
||||
protected boolean supportsAutomaticRetry(String requestMethod) {
|
||||
return StringUtil.noText(requestMethod)
|
||||
|| "GET".equalsIgnoreCase(requestMethod)
|
||||
|| "HEAD".equalsIgnoreCase(requestMethod)
|
||||
|| "OPTIONS".equalsIgnoreCase(requestMethod);
|
||||
}
|
||||
|
||||
private RuntimeException wrapAsRuntime(Throwable ex, int attempt) {
|
||||
if (ex instanceof RuntimeException) {
|
||||
return (RuntimeException) ex;
|
||||
@@ -212,13 +237,18 @@ public class HttpNode extends BaseNode {
|
||||
|
||||
public Map<String, Object> doExecute(Chain chain) throws IOException {
|
||||
|
||||
Map<String, Object> argsMap = chain.getState().resolveParameters(this);
|
||||
Map<String, Object> argsMap =
|
||||
chain.getExecutionState().resolveParameters(this);
|
||||
String newUrl = TextTemplate.of(url)
|
||||
.formatToString(chain.getState().buildTemplateRootMaps(argsMap));
|
||||
.formatToString(
|
||||
chain.getExecutionState()
|
||||
.buildTemplateRootMaps(argsMap));
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(newUrl);
|
||||
|
||||
Map<String, Object> headersMap = chain.getState().resolveParameters(this, headers, argsMap);
|
||||
Map<String, Object> headersMap =
|
||||
chain.getExecutionState().resolveParameters(
|
||||
this, headers, argsMap);
|
||||
headersMap.forEach((s, o) -> reqBuilder.addHeader(s, String.valueOf(o)));
|
||||
|
||||
if (StringUtil.noText(method) || "GET".equalsIgnoreCase(method)) {
|
||||
@@ -227,8 +257,12 @@ public class HttpNode extends BaseNode {
|
||||
reqBuilder.method(method.toUpperCase(), getRequestBody(chain, argsMap));
|
||||
}
|
||||
|
||||
OkHttpClient okHttpClient = OkHttpClientUtil.buildDefaultClient();
|
||||
try (Response response = okHttpClient.newCall(reqBuilder.build()).execute()) {
|
||||
// 节点层统一计算总尝试次数,禁用客户端隐式重试,避免乘法式放大。
|
||||
OkHttpClient okHttpClient =
|
||||
OkHttpClientUtil.buildNoRetryClient();
|
||||
// 共享客户端拦截器持有请求许可直到响应体关闭,避免同一请求重复领取许可。
|
||||
try (Response response = okHttpClient.newCall(
|
||||
reqBuilder.build()).execute()) {
|
||||
|
||||
// 服务器异常
|
||||
if (response.code() >= 500 && response.code() < 600) {
|
||||
@@ -263,17 +297,39 @@ public class HttpNode extends BaseNode {
|
||||
}
|
||||
|
||||
if (bodyDataType == null) {
|
||||
result.put("body", body.string());
|
||||
try (IoBulkhead.Permit ignored =
|
||||
IoBulkhead.responseAggregation().acquire(
|
||||
IoBulkhead.targetForUrl(newUrl))) {
|
||||
result.put("body", readTextBody(
|
||||
body, resolveMaxTextResponseBytes()));
|
||||
}
|
||||
} else if (bodyDataType == DataType.Object || bodyDataType.getValue().startsWith("Array")) {
|
||||
result.put("body", JSON.parse(body.string()));
|
||||
try (IoBulkhead.Permit ignored =
|
||||
IoBulkhead.responseAggregation().acquire(
|
||||
IoBulkhead.targetForUrl(newUrl))) {
|
||||
result.put("body", JSON.parse(readTextBody(
|
||||
body, resolveMaxTextResponseBytes())));
|
||||
}
|
||||
} else if (bodyDataType == DataType.File) {
|
||||
try (InputStream stream = body.byteStream()) {
|
||||
long maxFileResponseBytes = resolveMaxFileResponseBytes();
|
||||
validateDeclaredResponseSize(body, maxFileResponseBytes);
|
||||
try (InputStream stream = limitResponseStream(body.byteStream(), maxFileResponseBytes)) {
|
||||
FileStorage fileStorage = FileStorageManager.getInstance().getFileStorage();
|
||||
String fileUrl = fileStorage.saveFile(stream, responseHeaders, this, chain);
|
||||
try (IoBulkhead.Permit ignored =
|
||||
IoBulkhead.storage().acquire(
|
||||
"storage:http-response")) {
|
||||
String fileUrl = fileStorage.saveFile(
|
||||
stream, responseHeaders, this, chain);
|
||||
result.put("body", fileUrl);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.put("body", body.string());
|
||||
try (IoBulkhead.Permit ignored =
|
||||
IoBulkhead.responseAggregation().acquire(
|
||||
IoBulkhead.targetForUrl(newUrl))) {
|
||||
result.put("body", readTextBody(
|
||||
body, resolveMaxTextResponseBytes()));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -282,19 +338,26 @@ public class HttpNode extends BaseNode {
|
||||
private RequestBody getRequestBody(Chain chain, Map<String, Object> formatArgs) {
|
||||
if ("json".equals(bodyType)) {
|
||||
String bodyJsonString = TextTemplate.of(bodyJson)
|
||||
.formatToString(chain.getState().buildTemplateContextMap(formatArgs), true);
|
||||
.formatToString(
|
||||
chain.getExecutionState()
|
||||
.buildTemplateRootMaps(formatArgs),
|
||||
true);
|
||||
JSONObject jsonObject = JSON.parseObject(bodyJsonString);
|
||||
return RequestBody.create(jsonObject.toString(), MediaType.parse("application/json"));
|
||||
}
|
||||
|
||||
if ("x-www-form-urlencoded".equals(bodyType)) {
|
||||
Map<String, Object> formUrlencodedMap = chain.getState().resolveParameters(this, formUrlencoded);
|
||||
Map<String, Object> formUrlencodedMap =
|
||||
chain.getExecutionState().resolveParameters(
|
||||
this, formUrlencoded);
|
||||
String bodyString = mapToQueryString(formUrlencodedMap);
|
||||
return RequestBody.create(bodyString, MediaType.parse("application/x-www-form-urlencoded"));
|
||||
}
|
||||
|
||||
if ("form-data".equals(bodyType)) {
|
||||
Map<String, Object> formDataMap = chain.getState().resolveParameters(this, formData, formatArgs);
|
||||
Map<String, Object> formDataMap =
|
||||
chain.getExecutionState().resolveParameters(
|
||||
this, formData, formatArgs);
|
||||
|
||||
MultipartBody.Builder builder = new MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM);
|
||||
@@ -320,13 +383,97 @@ public class HttpNode extends BaseNode {
|
||||
|
||||
if ("raw".equals(bodyType)) {
|
||||
String rawBodyString = TextTemplate.of(rawBody)
|
||||
.formatToString(chain.getState().buildTemplateRootMaps(formatArgs));
|
||||
.formatToString(
|
||||
chain.getExecutionState()
|
||||
.buildTemplateRootMaps(formatArgs));
|
||||
return RequestBody.create(rawBodyString, null);
|
||||
}
|
||||
//none
|
||||
return RequestBody.create("", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在宽松响应大小保护下读取文本响应。
|
||||
*
|
||||
* @param body HTTP 响应体
|
||||
* @return 响应文本
|
||||
* @throws IOException 响应读取失败或超出限制时抛出
|
||||
*/
|
||||
protected String readTextBody(ResponseBody body, long maxResponseBytes) throws IOException {
|
||||
validateDeclaredResponseSize(body, maxResponseBytes);
|
||||
Charset charset = body.contentType() == null
|
||||
? StandardCharsets.UTF_8
|
||||
: body.contentType().charset(StandardCharsets.UTF_8);
|
||||
int initialCapacity = body.contentLength() > 0L
|
||||
? (int) Math.min(body.contentLength(), 64L * 1024L)
|
||||
: 8 * 1024;
|
||||
try (InputStream input = limitResponseStream(body.byteStream(), maxResponseBytes);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream(initialCapacity)) {
|
||||
byte[] buffer = new byte[64 * 1024];
|
||||
int read;
|
||||
while ((read = input.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, read);
|
||||
}
|
||||
return output.toString(charset);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验响应声明长度,避免继续处理已知超限内容。
|
||||
*
|
||||
* @param body HTTP 响应体
|
||||
* @param maxResponseBytes 最大允许字节数
|
||||
* @throws IOException 声明长度超过限制时抛出
|
||||
*/
|
||||
private void validateDeclaredResponseSize(ResponseBody body, long maxResponseBytes) throws IOException {
|
||||
long contentLength = body.contentLength();
|
||||
if (maxResponseBytes > 0L && contentLength > maxResponseBytes) {
|
||||
throw responseSizeExceeded(maxResponseBytes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为响应流增加按实际读取字节数执行的限制。
|
||||
*
|
||||
* @param inputStream 原始响应流
|
||||
* @param maxResponseBytes 最大允许字节数
|
||||
* @return 受限响应流
|
||||
*/
|
||||
private InputStream limitResponseStream(InputStream inputStream, long maxResponseBytes) {
|
||||
if (maxResponseBytes <= 0L) {
|
||||
return inputStream;
|
||||
}
|
||||
return new LimitedResponseInputStream(inputStream, maxResponseBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文本或 JSON 响应字节数上限。
|
||||
*
|
||||
* @return 最大允许字节数
|
||||
*/
|
||||
private long resolveMaxTextResponseBytes() {
|
||||
return Long.getLong(MAX_RESPONSE_BYTES_PROPERTY, DEFAULT_MAX_TEXT_RESPONSE_BYTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件响应字节数上限。
|
||||
*
|
||||
* @return 最大允许字节数
|
||||
*/
|
||||
private long resolveMaxFileResponseBytes() {
|
||||
return Long.getLong(MAX_FILE_RESPONSE_BYTES_PROPERTY, DEFAULT_MAX_FILE_RESPONSE_BYTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建统一的响应超限异常。
|
||||
*
|
||||
* @param maxResponseBytes 最大允许字节数
|
||||
* @return 响应超限异常
|
||||
*/
|
||||
private IOException responseSizeExceeded(long maxResponseBytes) {
|
||||
return new IOException("HTTP response body exceeds limit: " + maxResponseBytes + " bytes");
|
||||
}
|
||||
|
||||
public static class HttpServerErrorException extends IOException {
|
||||
private final int statusCode;
|
||||
|
||||
@@ -340,6 +487,81 @@ public class HttpNode extends BaseNode {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按实际读取量限制 HTTP 响应大小的输入流。
|
||||
*/
|
||||
private final class LimitedResponseInputStream extends FilterInputStream {
|
||||
|
||||
private final long maxBytes;
|
||||
private long consumed;
|
||||
|
||||
/**
|
||||
* 创建受限响应流。
|
||||
*
|
||||
* @param inputStream 原始输入流
|
||||
* @param maxBytes 最大允许字节数
|
||||
*/
|
||||
private LimitedResponseInputStream(InputStream inputStream, long maxBytes) {
|
||||
super(inputStream);
|
||||
this.maxBytes = maxBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取单个字节并校验累计读取量。
|
||||
*
|
||||
* @return 读取的字节或 {@code -1}
|
||||
* @throws IOException 读取失败或超过限制时抛出
|
||||
*/
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
int value = super.read();
|
||||
if (value >= 0) {
|
||||
recordRead(1L);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量读取并校验累计读取量。
|
||||
*
|
||||
* @param buffer 目标缓冲区
|
||||
* @param offset 写入偏移
|
||||
* @param length 最大读取长度
|
||||
* @return 实际读取长度或 {@code -1}
|
||||
* @throws IOException 读取失败或超过限制时抛出
|
||||
*/
|
||||
@Override
|
||||
public int read(byte[] buffer, int offset, int length) throws IOException {
|
||||
long remainingWithProbe = maxBytes == Long.MAX_VALUE
|
||||
? Long.MAX_VALUE
|
||||
: maxBytes - consumed + 1L;
|
||||
int allowed = (int) Math.min(
|
||||
Math.max(0L, remainingWithProbe),
|
||||
(long) length);
|
||||
if (allowed <= 0) {
|
||||
throw responseSizeExceeded(maxBytes);
|
||||
}
|
||||
int count = super.read(buffer, offset, allowed);
|
||||
if (count > 0) {
|
||||
recordRead(count);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录实际读取量。
|
||||
*
|
||||
* @param count 本次读取字节数
|
||||
* @throws IOException 超过限制时抛出
|
||||
*/
|
||||
private void recordRead(long count) throws IOException {
|
||||
consumed += count;
|
||||
if (consumed > maxBytes) {
|
||||
throw responseSizeExceeded(maxBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package com.easyagents.flow.core.node;
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.ChainState;
|
||||
import com.easyagents.flow.core.knowledge.Knowledge;
|
||||
import com.easyagents.flow.core.knowledge.KnowledgeManager;
|
||||
import com.easyagents.flow.core.util.Maps;
|
||||
@@ -29,6 +30,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class KnowledgeNode extends BaseNode {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
private static final Logger logger = org.slf4j.LoggerFactory.getLogger(KnowledgeNode.class);
|
||||
|
||||
@@ -71,11 +74,16 @@ public class KnowledgeNode extends BaseNode {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
Map<String, Object> argsMap = chain.getState().resolveParameters(this);
|
||||
ChainState chainState =
|
||||
chain.getExecutionState();
|
||||
Map<String, Object> argsMap =
|
||||
chainState.resolveParameters(this);
|
||||
List<Map<String, Object>> templateRootMaps =
|
||||
chainState.buildTemplateRootMaps(argsMap);
|
||||
String realKeyword = TextTemplate.of(keyword)
|
||||
.formatToString(chain.getState().buildTemplateRootMaps(argsMap));
|
||||
.formatToString(templateRootMaps);
|
||||
String realLimitString = TextTemplate.of(limit)
|
||||
.formatToString(chain.getState().buildTemplateRootMaps(argsMap));
|
||||
.formatToString(templateRootMaps);
|
||||
int realLimit = 10;
|
||||
if (StringUtil.hasText(realLimitString)) {
|
||||
try {
|
||||
|
||||
@@ -17,6 +17,7 @@ package com.easyagents.flow.core.node;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.ChainState;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.llm.Llm;
|
||||
import com.easyagents.flow.core.llm.LlmManager;
|
||||
@@ -26,6 +27,8 @@ import java.io.File;
|
||||
import java.util.*;
|
||||
|
||||
public class LlmNode extends BaseNode {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
protected String llmId;
|
||||
protected Llm.ChatOptions chatOptions;
|
||||
@@ -88,14 +91,20 @@ public class LlmNode extends BaseNode {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
Map<String, Object> parameterValues = chain.getState().resolveParameters(this);
|
||||
ChainState chainState =
|
||||
chain.getExecutionState();
|
||||
Map<String, Object> parameterValues =
|
||||
chainState.resolveParameters(this);
|
||||
|
||||
if (StringUtil.noText(userPrompt)) {
|
||||
throw new RuntimeException("Can not find user prompt");
|
||||
}
|
||||
|
||||
List<Map<String, Object>> templateRootMaps =
|
||||
chainState.buildTemplateRootMaps(
|
||||
parameterValues);
|
||||
String userPromptString = TextTemplate.of(userPrompt)
|
||||
.formatToString(chain.getState().buildTemplateRootMaps(parameterValues));
|
||||
.formatToString(templateRootMaps);
|
||||
|
||||
|
||||
Llm llm = LlmManager.getInstance().getChatModel(this.llmId);
|
||||
@@ -104,14 +113,16 @@ public class LlmNode extends BaseNode {
|
||||
}
|
||||
|
||||
String systemPromptString = TextTemplate.of(this.systemPrompt)
|
||||
.formatToString(chain.getState().buildTemplateRootMaps(parameterValues));
|
||||
.formatToString(templateRootMaps);
|
||||
|
||||
Llm.MessageInfo messageInfo = new Llm.MessageInfo();
|
||||
messageInfo.setMessage(userPromptString);
|
||||
messageInfo.setSystemMessage(systemPromptString);
|
||||
|
||||
if (images != null && !images.isEmpty()) {
|
||||
Map<String, Object> filesMap = chain.getState().resolveParameters(this, images);
|
||||
Map<String, Object> filesMap =
|
||||
chainState.resolveParameters(
|
||||
this, images);
|
||||
List<String> imagesUrls = new ArrayList<>();
|
||||
filesMap.forEach((s, o) -> {
|
||||
if (o instanceof String) {
|
||||
|
||||
@@ -18,19 +18,30 @@ package com.easyagents.flow.core.node;
|
||||
|
||||
import com.easyagents.flow.core.chain.*;
|
||||
import com.easyagents.flow.core.chain.repository.ChainStateField;
|
||||
import com.easyagents.flow.core.chain.repository.LoopInputReference;
|
||||
import com.easyagents.flow.core.chain.repository.NodeStateField;
|
||||
import com.easyagents.flow.core.chain.runtime.Trigger;
|
||||
import com.easyagents.flow.core.chain.runtime.TriggerContext;
|
||||
import com.easyagents.flow.core.chain.runtime.TriggerType;
|
||||
import com.easyagents.flow.core.util.IterableUtil;
|
||||
import com.easyagents.flow.core.chain.runtime.ExecutionBudgetExceededException;
|
||||
import com.easyagents.flow.core.util.Maps;
|
||||
import com.easyagents.flow.core.util.StringUtil;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Array;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class LoopNode extends BaseNode {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final int DIRECT_INDEX_MAX_ITEMS = Math.max(
|
||||
1,
|
||||
Integer.getInteger(
|
||||
"tinyflow.loop.direct-index.max-items", 64));
|
||||
|
||||
private Parameter loopVar;
|
||||
|
||||
@@ -44,6 +55,43 @@ public class LoopNode extends BaseNode {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
MaterializationPlan[] planHolder = new MaterializationPlan[1];
|
||||
Map<String, Object> initialResult = chain.executeWithLock(
|
||||
chain.getStateInstanceId(),
|
||||
10,
|
||||
TimeUnit.SECONDS,
|
||||
() -> executeLocked(chain, false, planHolder));
|
||||
MaterializationPlan plan = planHolder[0];
|
||||
if (plan == null) {
|
||||
return initialResult;
|
||||
}
|
||||
|
||||
int iterableSize = chain.storeLoopInputOutsideLock(
|
||||
plan.resultId,
|
||||
plan.items,
|
||||
materializationLimit(chain),
|
||||
plan.claimId,
|
||||
plan.claimGeneration);
|
||||
return chain.executeWithLock(
|
||||
chain.getStateInstanceId(),
|
||||
10,
|
||||
TimeUnit.SECONDS,
|
||||
() -> publishMaterializedInputAndContinue(
|
||||
chain, plan, iterableSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* 在实例锁内推进一次循环状态机。
|
||||
*
|
||||
* @param chain 当前工作流
|
||||
* @param resumeAfterMaterialization 是否在本次调用中完成了锁外物化
|
||||
* @param planHolder 待锁外执行的物化计划容器
|
||||
* @return 节点执行结果
|
||||
*/
|
||||
private Map<String, Object> executeLocked(
|
||||
Chain chain,
|
||||
boolean resumeAfterMaterialization,
|
||||
MaterializationPlan[] planHolder) {
|
||||
Trigger prevTrigger = TriggerContext.getCurrentTrigger();
|
||||
Deque<LoopContext> loopStack = getOrCreateLoopStack(chain);
|
||||
|
||||
@@ -51,15 +99,22 @@ public class LoopNode extends BaseNode {
|
||||
|
||||
// 判断是否是首次进入该 LoopNode(即不是由子节点返回)
|
||||
TriggerType triggerType = prevTrigger.getType();
|
||||
boolean isFirstEntry = triggerType != TriggerType.PARENT && triggerType != TriggerType.SELF;
|
||||
boolean isFirstEntry = !resumeAfterMaterialization
|
||||
&& triggerType != TriggerType.PARENT
|
||||
&& triggerType != TriggerType.SELF;
|
||||
|
||||
if (isFirstEntry) {
|
||||
// 首次触发:创建新的 LoopContext 并压入堆栈
|
||||
loopContext = new LoopContext();
|
||||
loopContext.currentIndex = 0;
|
||||
loopContext.subResult = new HashMap<>();
|
||||
loopContext.resultId = chain.getStateInstanceId() + ":" + UUID.randomUUID();
|
||||
// 保存原始触发上下文(用于循环结束后恢复)
|
||||
loopStack.offerLast(loopContext);
|
||||
int nestedDepth = chain.getNestedDepthBase()
|
||||
+ (prevTrigger == null
|
||||
? 1
|
||||
: prevTrigger.getLoopCursors().size() + 1);
|
||||
chain.getExecutionBudget().checkNestedDepth(this.id, nestedDepth);
|
||||
|
||||
chain.updateNodeStateSafely(this.id, state -> {
|
||||
state.getMemory().put(buildLoopStackId(), loopStack);
|
||||
@@ -83,33 +138,114 @@ public class LoopNode extends BaseNode {
|
||||
loopContext = loopStack.peekFirst();
|
||||
}
|
||||
|
||||
if (loopContext.materializingInput && !resumeAfterMaterialization) {
|
||||
String currentClaimId = chain.currentFencingClaimId();
|
||||
long currentGeneration = chain.currentClaimGeneration();
|
||||
if (Objects.equals(loopContext.materializationClaimId, currentClaimId)
|
||||
&& loopContext.materializationClaimGeneration == currentGeneration) {
|
||||
return waitingResult();
|
||||
}
|
||||
// 原 owner 已失去 claim,由新代际使用全新 resultId 接管。
|
||||
loopContext.resultId =
|
||||
chain.getStateInstanceId() + ":" + UUID.randomUUID();
|
||||
loopContext.materializationClaimId = currentClaimId;
|
||||
loopContext.materializationClaimGeneration = currentGeneration;
|
||||
persistLoopStack(chain, loopStack);
|
||||
}
|
||||
|
||||
// LoopContext loopContext = getLoopContext(prevTrigger, chain);
|
||||
// int triggerLoopIndex = getTriggerLoopIndex(prevTrigger);
|
||||
//
|
||||
// if (loopContext.currentIndex != triggerLoopIndex) {
|
||||
// // 不执行,子流程有分叉,已经被其他的分叉节点触发了
|
||||
// return Maps.of(ChainConsts.SCHEDULE_NEXT_NODE_DISABLED_KEY, true)
|
||||
// .set(ChainConsts.NODE_STATE_STATUS_KEY, NodeStatus.RUNNING);
|
||||
// }
|
||||
|
||||
Map<String, Object> loopVars = chain.getState().resolveParameters(this, Collections.singletonList(loopVar));
|
||||
Object loopValue = loopVars.get(loopVar.getName());
|
||||
migrateLegacyResult(chain, loopContext);
|
||||
if (!acceptParentBranch(prevTrigger, loopContext, chain, loopStack)) {
|
||||
return Maps.of(ChainConsts.SCHEDULE_NEXT_NODE_DISABLED_KEY, true)
|
||||
.set(ChainConsts.NODE_STATE_STATUS_KEY, NodeStatus.RUNNING);
|
||||
}
|
||||
|
||||
int shouldLoopCount;
|
||||
if (loopValue instanceof Iterable) {
|
||||
shouldLoopCount = IterableUtil.size((Iterable<?>) loopValue);
|
||||
} else if (loopValue instanceof Number || (loopValue instanceof String && StringUtil.isNumeric(loopValue.toString()))) {
|
||||
shouldLoopCount = loopValue instanceof Number ? ((Number) loopValue).intValue() : Integer.parseInt(loopValue.toString().trim());
|
||||
boolean storedIterable = false;
|
||||
boolean directlyIndexed = false;
|
||||
boolean numericLoop = false;
|
||||
Object loopValue = null;
|
||||
if (loopContext.iterableInputStored) {
|
||||
// 已物化输入不再解析原始参数,避免大集合在每轮反序列化和路径解析。
|
||||
shouldLoopCount = loopContext.iterableSize;
|
||||
storedIterable = true;
|
||||
} else {
|
||||
throw new IllegalArgumentException("loopValue must be Iterable or Number or String, but loopValue is \"" + loopValue + "\"");
|
||||
LoopInputReference storedInput =
|
||||
resolveStoredInputReference(chain);
|
||||
if (storedInput != null) {
|
||||
loopContext.resultId =
|
||||
storedInput.getResultId();
|
||||
loopContext.iterableSize =
|
||||
storedInput.getItemCount();
|
||||
loopContext.iterableInputStored = true;
|
||||
loopContext.inputExternalized = true;
|
||||
shouldLoopCount =
|
||||
storedInput.getItemCount();
|
||||
storedIterable = true;
|
||||
persistLoopStack(chain, loopStack);
|
||||
} else {
|
||||
Map<String, Object> loopVars =
|
||||
chain.getExecutionState().resolveParameters(
|
||||
this,
|
||||
Collections.singletonList(
|
||||
loopVar));
|
||||
loopValue = loopVars.get(loopVar.getName());
|
||||
Iterable<?> iterableInput =
|
||||
toIterableInput(loopValue);
|
||||
if (iterableInput != null) {
|
||||
int knownSize = knownInputSize(loopValue);
|
||||
if (knownSize >= 0) {
|
||||
checkExplicitLoopIterations(chain, knownSize, true);
|
||||
}
|
||||
if (knownSize >= 0
|
||||
&& knownSize <= DIRECT_INDEX_MAX_ITEMS) {
|
||||
shouldLoopCount = knownSize;
|
||||
directlyIndexed = true;
|
||||
} else {
|
||||
/*
|
||||
* 每次未完成的物化尝试使用新结果 ID。失锁 owner 的旧分块仅会按 TTL
|
||||
* 回收,接管者不会复用或删除其部分数据。
|
||||
*/
|
||||
loopContext.resultId =
|
||||
chain.getStateInstanceId() + ":" + UUID.randomUUID();
|
||||
loopContext.materializingInput = true;
|
||||
loopContext.materializationClaimId =
|
||||
chain.currentFencingClaimId();
|
||||
loopContext.materializationClaimGeneration =
|
||||
chain.currentClaimGeneration();
|
||||
persistLoopStack(chain, loopStack);
|
||||
if (planHolder == null) {
|
||||
throw new IllegalStateException(
|
||||
"Loop materialization plan holder is unavailable");
|
||||
}
|
||||
planHolder[0] = new MaterializationPlan(
|
||||
loopContext.resultId,
|
||||
iterableInput,
|
||||
loopContext.materializationClaimId,
|
||||
loopContext.materializationClaimGeneration);
|
||||
return waitingResult();
|
||||
}
|
||||
} else if (loopValue instanceof Number
|
||||
|| loopValue instanceof String) {
|
||||
shouldLoopCount = parseNumericLoopCount(loopValue);
|
||||
numericLoop = true;
|
||||
} else {
|
||||
throw invalidLoopValue(loopValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
checkExplicitLoopIterations(chain, shouldLoopCount, !numericLoop);
|
||||
|
||||
// 不是第一次执行,合并结果到 subResult
|
||||
if (loopContext.currentIndex != 0) {
|
||||
ChainState subState = chain.getState();
|
||||
ChainState subState =
|
||||
chain.getExecutionState();
|
||||
Map<String, Object> currentOutputs = collectCurrentOutputValues(subState);
|
||||
mergeResult(loopContext.subResult, currentOutputs);
|
||||
loopContext.accumulatedBytes += estimateBytes(currentOutputs, new IdentityHashMap<>());
|
||||
chain.getExecutionBudget().checkAccumulatedBytes(this.id, loopContext.accumulatedBytes);
|
||||
chain.appendLoopResult(
|
||||
loopContext.resultId,
|
||||
loopContext.currentIndex - 1,
|
||||
currentOutputs);
|
||||
// 将上一轮最新输出同步到循环节点作用域,供下一轮循环体读取。
|
||||
publishLoopProgress(chain, currentOutputs);
|
||||
}
|
||||
@@ -128,25 +264,37 @@ public class LoopNode extends BaseNode {
|
||||
if (!loopStack.isEmpty()) {
|
||||
chain.scheduleNode(this, null, TriggerType.SELF, 0);
|
||||
}
|
||||
return loopContext.subResult;
|
||||
if (prevTrigger != null) {
|
||||
prevTrigger.getLoopCursors().remove(this.id);
|
||||
}
|
||||
Map<String, Object> completedResult = chain.getLoopResultRepository().references(
|
||||
loopContext.resultId, loopContext.currentIndex, getOutputNames());
|
||||
chain.getLoopResultRepository().releaseActiveCache(
|
||||
loopContext.resultId);
|
||||
if (!loopContext.inputExternalized) {
|
||||
chain.removeLoopInput(loopContext.resultId);
|
||||
}
|
||||
return completedResult;
|
||||
}
|
||||
|
||||
int loopIndex = loopContext.currentIndex;
|
||||
loopContext.currentIndex++;
|
||||
|
||||
chain.updateNodeStateSafely(this.id, state -> {
|
||||
state.getMemory().put(buildLoopStackId(), loopStack);
|
||||
return EnumSet.of(NodeStateField.MEMORY);
|
||||
});
|
||||
persistLoopStack(chain, loopStack);
|
||||
|
||||
|
||||
if (loopValue instanceof Iterable) {
|
||||
Object loopItem = IterableUtil.get((Iterable<?>) loopValue, loopIndex);
|
||||
if (storedIterable) {
|
||||
Object loopItem = chain.getLoopResultRepository().loadInputItem(loopContext.resultId, loopIndex);
|
||||
executeLoopChain(chain, loopContext, loopItem);
|
||||
} else if (loopValue instanceof Number || (loopValue instanceof String && StringUtil.isNumeric(loopValue.toString()))) {
|
||||
} else if (directlyIndexed) {
|
||||
executeLoopChain(
|
||||
chain,
|
||||
loopContext,
|
||||
directInputItem(loopValue, loopIndex));
|
||||
} else if (numericLoop) {
|
||||
executeLoopChain(chain, loopContext, loopIndex);
|
||||
} else {
|
||||
throw new IllegalArgumentException("loopValue must be Iterable or Number or String, but loopValue is \"" + loopValue + "\"");
|
||||
throw invalidLoopValue(loopValue);
|
||||
}
|
||||
|
||||
// 禁用调度下个节点
|
||||
@@ -154,6 +302,281 @@ public class LoopNode extends BaseNode {
|
||||
.set(ChainConsts.NODE_STATE_STATUS_KEY, NodeStatus.RUNNING);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在短实例锁内发布锁外物化结果并继续循环状态机。
|
||||
*
|
||||
* @param chain 当前工作流
|
||||
* @param plan 已完成的物化计划
|
||||
* @param iterableSize 物化元素数
|
||||
* @return 本轮循环执行结果
|
||||
*/
|
||||
private Map<String, Object> publishMaterializedInputAndContinue(
|
||||
Chain chain, MaterializationPlan plan, int iterableSize) {
|
||||
if (!chain.isExecutionActiveNow()) {
|
||||
throw new com.easyagents.flow.core.chain.runtime.TriggerClaimLostException(
|
||||
"loop-materialization-cancelled:" + plan.resultId);
|
||||
}
|
||||
Deque<LoopContext> loopStack = getOrCreateLoopStack(chain);
|
||||
LoopContext context = loopStack.peekFirst();
|
||||
if (context == null
|
||||
|| !context.materializingInput
|
||||
|| !Objects.equals(context.resultId, plan.resultId)
|
||||
|| !Objects.equals(
|
||||
context.materializationClaimId, plan.claimId)
|
||||
|| context.materializationClaimGeneration
|
||||
!= plan.claimGeneration) {
|
||||
throw new com.easyagents.flow.core.chain.runtime.TriggerClaimLostException(
|
||||
"loop-materialization:" + plan.resultId);
|
||||
}
|
||||
context.iterableSize = iterableSize;
|
||||
context.iterableInputStored = true;
|
||||
context.materializingInput = false;
|
||||
context.inputExternalized = externalizeMaterializedInput(
|
||||
chain, plan.resultId, iterableSize);
|
||||
persistLoopStack(chain, loopStack);
|
||||
return executeLocked(chain, true, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将热状态中的大型原始输入替换为轻量引用。
|
||||
*
|
||||
* <p>仅替换参数直接对应的扁平内存键;无法准确定位的嵌套路径保持原值,
|
||||
* 以业务兼容性优先。其他节点读取该引用时由仓储透明还原完整列表。</p>
|
||||
*
|
||||
* @param chain 当前工作流
|
||||
* @param resultId 已物化输入 ID
|
||||
* @param iterableSize 输入元素数
|
||||
* @return 已替换热状态值时为 {@code true}
|
||||
*/
|
||||
private boolean externalizeMaterializedInput(
|
||||
Chain chain, String resultId, int iterableSize) {
|
||||
String ref = loopVar == null ? null : loopVar.getRef();
|
||||
String name = loopVar == null ? null : loopVar.getName();
|
||||
AtomicBoolean replaced = new AtomicBoolean();
|
||||
chain.updateStateSafely(state -> {
|
||||
ConcurrentHashMap<String, Object> memory =
|
||||
state.getMemory();
|
||||
String key = StringUtil.hasText(ref)
|
||||
&& memory.containsKey(ref)
|
||||
? ref
|
||||
: (StringUtil.hasText(name)
|
||||
&& memory.containsKey(name)
|
||||
? name
|
||||
: null);
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
Object current = memory.get(key);
|
||||
if (current instanceof LoopInputReference) {
|
||||
replaced.set(true);
|
||||
return null;
|
||||
}
|
||||
memory.put(
|
||||
key,
|
||||
new LoopInputReference(
|
||||
resultId, iterableSize));
|
||||
replaced.set(true);
|
||||
return EnumSet.of(ChainStateField.MEMORY);
|
||||
});
|
||||
return replaced.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接识别上游已分页物化的输入引用,避免参数解析边界先还原完整列表。
|
||||
*
|
||||
* @param chain 当前工作流
|
||||
* @return 已物化输入引用;不存在时为 {@code null}
|
||||
*/
|
||||
private LoopInputReference resolveStoredInputReference(
|
||||
Chain chain) {
|
||||
if (loopVar == null) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Object> memory =
|
||||
chain.getExecutionState().getMemory();
|
||||
String ref = loopVar.getRef();
|
||||
if (StringUtil.hasText(ref)
|
||||
&& memory.get(ref)
|
||||
instanceof LoopInputReference) {
|
||||
return (LoopInputReference) memory.get(ref);
|
||||
}
|
||||
String name = loopVar.getName();
|
||||
return StringUtil.hasText(name)
|
||||
&& memory.get(name)
|
||||
instanceof LoopInputReference
|
||||
? (LoopInputReference) memory.get(name)
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造不推进下游的循环等待结果。
|
||||
*
|
||||
* @return 运行态控制结果
|
||||
*/
|
||||
private Map<String, Object> waitingResult() {
|
||||
return Maps.of(ChainConsts.SCHEDULE_NEXT_NODE_DISABLED_KEY, true)
|
||||
.set(ChainConsts.NODE_STATE_STATUS_KEY, NodeStatus.RUNNING);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将集合或数组统一转换为单次消费的 Iterable。
|
||||
*
|
||||
* @param loopValue 循环输入
|
||||
* @return 可迭代输入;数值循环返回 {@code null}
|
||||
*/
|
||||
private Iterable<?> toIterableInput(Object loopValue) {
|
||||
if (loopValue instanceof Iterable<?>) {
|
||||
return (Iterable<?>) loopValue;
|
||||
}
|
||||
if (loopValue != null && loopValue.getClass().isArray()) {
|
||||
int length = Array.getLength(loopValue);
|
||||
return () -> new Iterator<Object>() {
|
||||
private int index;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return index < length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object next() {
|
||||
if (!hasNext()) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
return Array.get(loopValue, index++);
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取无需遍历即可确定的输入元素数量。
|
||||
*
|
||||
* @param loopValue 循环输入
|
||||
* @return 已知数量;未知时返回 {@code -1}
|
||||
*/
|
||||
private int knownInputSize(Object loopValue) {
|
||||
if (loopValue instanceof Collection<?>) {
|
||||
return ((Collection<?>) loopValue).size();
|
||||
}
|
||||
if (loopValue != null && loopValue.getClass().isArray()) {
|
||||
return Array.getLength(loopValue);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析数值型循环次数,循环索引从 0 开始并执行到 {@code count - 1}。
|
||||
*
|
||||
* @param loopValue 数值或数值字符串
|
||||
* @return 1~300 的循环总次数
|
||||
* @throws IllegalArgumentException 输入不是范围内整数
|
||||
*/
|
||||
private int parseNumericLoopCount(Object loopValue) {
|
||||
final int count;
|
||||
try {
|
||||
count = new BigDecimal(String.valueOf(loopValue).trim())
|
||||
.intValueExact();
|
||||
} catch (ArithmeticException | NumberFormatException exception) {
|
||||
throw new IllegalArgumentException(
|
||||
"Loop count must be an integer between "
|
||||
+ Node.MIN_LOOP_COUNT
|
||||
+ " and "
|
||||
+ Node.MAX_LOOP_COUNT,
|
||||
exception);
|
||||
}
|
||||
if (count < Node.MIN_LOOP_COUNT || count > Node.MAX_LOOP_COUNT) {
|
||||
throw new IllegalArgumentException(
|
||||
"Loop count must be between "
|
||||
+ Node.MIN_LOOP_COUNT
|
||||
+ " and "
|
||||
+ Node.MAX_LOOP_COUNT
|
||||
+ ", but was "
|
||||
+ count);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验显式循环节点的本层迭代次数。
|
||||
*
|
||||
* @param chain 当前工作流
|
||||
* @param iterations 本层计划迭代次数
|
||||
* @param allowEmpty 是否允许空集合产生零次迭代
|
||||
* @throws IllegalArgumentException 不允许的负数或零次数
|
||||
* @throws ExecutionBudgetExceededException 超过 300 次或部署预算
|
||||
*/
|
||||
private void checkExplicitLoopIterations(
|
||||
Chain chain, long iterations, boolean allowEmpty) {
|
||||
if (iterations < 0L || (!allowEmpty && iterations == 0L)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Loop count must be at least " + Node.MIN_LOOP_COUNT);
|
||||
}
|
||||
if (iterations > Node.MAX_LOOP_COUNT) {
|
||||
throw new ExecutionBudgetExceededException(
|
||||
"Loop iteration limit exceeded for node "
|
||||
+ this.id
|
||||
+ ": "
|
||||
+ iterations
|
||||
+ " > "
|
||||
+ Node.MAX_LOOP_COUNT);
|
||||
}
|
||||
chain.getExecutionBudget().checkIterations(this.id, iterations);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算未知 Iterable 的物化上限,同时服从平台预算和 300 次硬上限。
|
||||
*
|
||||
* @param chain 当前工作流
|
||||
* @return 正数物化上限
|
||||
*/
|
||||
private long materializationLimit(Chain chain) {
|
||||
long budgetLimit = chain.getExecutionBudget().getMaxIterations();
|
||||
if (budgetLimit <= 0L) {
|
||||
return Node.MAX_LOOP_COUNT;
|
||||
}
|
||||
return Math.min(budgetLimit, Node.MAX_LOOP_COUNT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按序号读取小型集合或数组,避免额外 Redis 分块读写。
|
||||
*
|
||||
* @param loopValue 小型循环输入
|
||||
* @param index 元素序号
|
||||
* @return 对应元素
|
||||
*/
|
||||
private Object directInputItem(Object loopValue, int index) {
|
||||
if (loopValue instanceof List<?>) {
|
||||
return ((List<?>) loopValue).get(index);
|
||||
}
|
||||
if (loopValue != null && loopValue.getClass().isArray()) {
|
||||
return Array.get(loopValue, index);
|
||||
}
|
||||
if (loopValue instanceof Collection<?>) {
|
||||
Iterator<?> iterator =
|
||||
((Collection<?>) loopValue).iterator();
|
||||
for (int current = 0; current < index; current++) {
|
||||
iterator.next();
|
||||
}
|
||||
return iterator.next();
|
||||
}
|
||||
throw invalidLoopValue(loopValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建循环输入类型错误。
|
||||
*
|
||||
* @param loopValue 非法输入
|
||||
* @return 参数异常
|
||||
*/
|
||||
private IllegalArgumentException invalidLoopValue(Object loopValue) {
|
||||
return new IllegalArgumentException(
|
||||
"loopValue must be Iterable, array, Number or String, but loopValue is \""
|
||||
+ loopValue
|
||||
+ "\"");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取或创建当前节点的 LoopContext 堆栈(每个 LoopNode 实例独立)
|
||||
@@ -168,15 +591,10 @@ public class LoopNode extends BaseNode {
|
||||
stack = (Deque<LoopContext>) stackObj;
|
||||
} else {
|
||||
stack = new ArrayDeque<>();
|
||||
chain.updateNodeStateSafely(this.id, state -> {
|
||||
state.getMemory().put(key, stack);
|
||||
return EnumSet.of(NodeStateField.MEMORY);
|
||||
});
|
||||
}
|
||||
return stack;
|
||||
}
|
||||
|
||||
|
||||
private void executeLoopChain(Chain chain, LoopContext loopContext, Object loopItem) {
|
||||
|
||||
chain.updateStateSafely(state -> {
|
||||
@@ -188,13 +606,116 @@ public class LoopNode extends BaseNode {
|
||||
|
||||
|
||||
ChainDefinition definition = chain.getDefinition();
|
||||
List<Edge> outwardEdges = definition.getOutwardEdge(this.id);
|
||||
for (Edge edge : outwardEdges) {
|
||||
Node childNode = definition.getNodeById(edge.getTarget());
|
||||
if (childNode.getParentId() != null && childNode.getParentId().equals(this.id)) {
|
||||
chain.scheduleNode(childNode, edge.getId(), TriggerType.CHILD, 0);
|
||||
List<ChainDefinition.LoopChildDispatch> childDispatches =
|
||||
definition.getLoopChildDispatches(this.id);
|
||||
if (childDispatches.isEmpty()) {
|
||||
throw new IllegalStateException("Loop node has no executable child branch: " + this.id);
|
||||
}
|
||||
loopContext.expectedReturnCount = childDispatches.size();
|
||||
loopContext.getCompletedBranchIds().clear();
|
||||
for (ChainDefinition.LoopChildDispatch dispatch :
|
||||
childDispatches) {
|
||||
Trigger.LoopCursor cursor = new Trigger.LoopCursor(
|
||||
loopContext.resultId,
|
||||
loopContext.currentIndex - 1,
|
||||
dispatch.getBranchId());
|
||||
chain.scheduleLoopChild(
|
||||
dispatch.getNode(),
|
||||
dispatch.getEdgeId(),
|
||||
0,
|
||||
this.id,
|
||||
cursor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验父分支回调的代际和分支屏障。
|
||||
*
|
||||
* @param trigger 当前触发器
|
||||
* @param context 当前循环上下文
|
||||
* @param chain 当前工作流
|
||||
* @param loopStack 循环上下文栈
|
||||
* @return 当前回调是否为本轮最后一个有效分支
|
||||
*/
|
||||
private boolean acceptParentBranch(Trigger trigger,
|
||||
LoopContext context,
|
||||
Chain chain,
|
||||
Deque<LoopContext> loopStack) {
|
||||
if (trigger == null || trigger.getType() != TriggerType.PARENT) {
|
||||
return true;
|
||||
}
|
||||
Trigger.LoopCursor cursor = trigger.getLoopCursors().get(this.id);
|
||||
if (cursor == null) {
|
||||
return true;
|
||||
}
|
||||
if (!Objects.equals(context.resultId, cursor.getResultId())) {
|
||||
return false;
|
||||
}
|
||||
int expectedIndex = context.currentIndex - 1;
|
||||
if (cursor.getIterationIndex() < expectedIndex) {
|
||||
return false;
|
||||
}
|
||||
if (cursor.getIterationIndex() > expectedIndex) {
|
||||
throw new IllegalStateException("Future loop callback index: " + cursor.getIterationIndex());
|
||||
}
|
||||
if (!context.getCompletedBranchIds().add(cursor.getBranchId())) {
|
||||
return false;
|
||||
}
|
||||
persistLoopStack(chain, loopStack);
|
||||
if (context.getCompletedBranchIds().size() < Math.max(1, context.expectedReturnCount)) {
|
||||
return false;
|
||||
}
|
||||
context.getCompletedBranchIds().clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将旧版本内嵌累计结果惰性迁移到分块仓储。
|
||||
*
|
||||
* @param chain 当前工作流
|
||||
* @param context 循环上下文
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void migrateLegacyResult(Chain chain, LoopContext context) {
|
||||
if (context.resultId != null) {
|
||||
return;
|
||||
}
|
||||
context.resultId = chain.getStateInstanceId() + ":" + UUID.randomUUID();
|
||||
if (context.subResult != null && !context.subResult.isEmpty()) {
|
||||
int migratedCount = 0;
|
||||
for (Object value : context.subResult.values()) {
|
||||
if (value instanceof List) {
|
||||
migratedCount = Math.max(migratedCount, ((List<?>) value).size());
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < migratedCount; index++) {
|
||||
Map<String, Object> outputs = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, Object> entry : context.subResult.entrySet()) {
|
||||
if (entry.getValue() instanceof List
|
||||
&& index < ((List<?>) entry.getValue()).size()) {
|
||||
outputs.put(entry.getKey(), ((List<Object>) entry.getValue()).get(index));
|
||||
}
|
||||
}
|
||||
chain.appendLoopResult(
|
||||
context.resultId,
|
||||
index,
|
||||
outputs);
|
||||
}
|
||||
}
|
||||
context.subResult = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存紧凑循环上下文。
|
||||
*
|
||||
* @param chain 当前工作流
|
||||
* @param loopStack 循环上下文栈
|
||||
*/
|
||||
private void persistLoopStack(Chain chain, Deque<LoopContext> loopStack) {
|
||||
chain.updateNodeStateSafely(this.id, state -> {
|
||||
state.getMemory().put(buildLoopStackId(), loopStack);
|
||||
return EnumSet.of(NodeStateField.MEMORY);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -227,20 +748,69 @@ public class LoopNode extends BaseNode {
|
||||
}
|
||||
|
||||
|
||||
private void mergeResult(Map<String, Object> toResult, Map<String, Object> currentOutputs) {
|
||||
/**
|
||||
* 获取循环输出名称并保持定义顺序。
|
||||
*
|
||||
* @return 输出名称列表
|
||||
*/
|
||||
private List<String> getOutputNames() {
|
||||
List<String> outputNames = new ArrayList<>();
|
||||
List<Parameter> outputDefs = getOutputDefs();
|
||||
if (outputDefs != null) {
|
||||
for (Parameter outputDef : outputDefs) {
|
||||
Object value = currentOutputs.get(outputDef.getName());
|
||||
outputNames.add(outputDef.getName());
|
||||
}
|
||||
}
|
||||
return outputNames;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked") List<Object> existList = (List<Object>) toResult.get(outputDef.getName());
|
||||
if (existList == null) {
|
||||
existList = new ArrayList<>();
|
||||
/**
|
||||
* 估算本轮输出占用字节数,用于宽松的累计结果失控保护。
|
||||
*
|
||||
* @param value 待估算值
|
||||
* @param visited 已访问对象集合,防止循环引用
|
||||
* @return 估算字节数
|
||||
*/
|
||||
private long estimateBytes(Object value, IdentityHashMap<Object, Boolean> visited) {
|
||||
if (value == null) {
|
||||
return 0L;
|
||||
}
|
||||
existList.add(value);
|
||||
toResult.put(outputDef.getName(), existList);
|
||||
if (value instanceof String) {
|
||||
return (long) ((String) value).length() * Character.BYTES;
|
||||
}
|
||||
if (value instanceof byte[]) {
|
||||
return ((byte[]) value).length;
|
||||
}
|
||||
if (value instanceof Number || value instanceof Date) {
|
||||
return 16L;
|
||||
}
|
||||
if (value instanceof Boolean || value instanceof Character) {
|
||||
return 2L;
|
||||
}
|
||||
if (visited.put(value, Boolean.TRUE) != null) {
|
||||
return 0L;
|
||||
}
|
||||
long bytes = 0L;
|
||||
if (value instanceof Map) {
|
||||
for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
|
||||
bytes += estimateBytes(entry.getKey(), visited);
|
||||
bytes += estimateBytes(entry.getValue(), visited);
|
||||
}
|
||||
} else if (value instanceof Collection) {
|
||||
for (Object item : (Collection<?>) value) {
|
||||
bytes += estimateBytes(item, visited);
|
||||
}
|
||||
} else if (value instanceof Iterable) {
|
||||
bytes = 64L;
|
||||
} else if (value.getClass().isArray()) {
|
||||
int length = Array.getLength(value);
|
||||
for (int index = 0; index < length; index++) {
|
||||
bytes += estimateBytes(Array.get(value, index), visited);
|
||||
}
|
||||
} else {
|
||||
bytes = 64L;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
|
||||
@@ -274,8 +844,20 @@ public class LoopNode extends BaseNode {
|
||||
|
||||
|
||||
public static class LoopContext implements Serializable {
|
||||
private static final long serialVersionUID = 5258356831772776243L;
|
||||
|
||||
int currentIndex;
|
||||
String resultId;
|
||||
Map<String, Object> subResult;
|
||||
boolean iterableInputStored;
|
||||
boolean inputExternalized;
|
||||
boolean materializingInput;
|
||||
String materializationClaimId;
|
||||
long materializationClaimGeneration;
|
||||
int iterableSize;
|
||||
long accumulatedBytes;
|
||||
int expectedReturnCount = 1;
|
||||
Set<String> completedBranchIds = new LinkedHashSet<>();
|
||||
|
||||
public int getCurrentIndex() {
|
||||
return currentIndex;
|
||||
@@ -285,6 +867,14 @@ public class LoopNode extends BaseNode {
|
||||
this.currentIndex = currentIndex;
|
||||
}
|
||||
|
||||
public String getResultId() {
|
||||
return resultId;
|
||||
}
|
||||
|
||||
public void setResultId(String resultId) {
|
||||
this.resultId = resultId;
|
||||
}
|
||||
|
||||
public Map<String, Object> getSubResult() {
|
||||
return subResult;
|
||||
}
|
||||
@@ -293,5 +883,111 @@ public class LoopNode extends BaseNode {
|
||||
this.subResult = subResult;
|
||||
}
|
||||
|
||||
public long getAccumulatedBytes() {
|
||||
return accumulatedBytes;
|
||||
}
|
||||
|
||||
public void setAccumulatedBytes(long accumulatedBytes) {
|
||||
this.accumulatedBytes = accumulatedBytes;
|
||||
}
|
||||
|
||||
public boolean isIterableInputStored() {
|
||||
return iterableInputStored;
|
||||
}
|
||||
|
||||
public void setIterableInputStored(boolean iterableInputStored) {
|
||||
this.iterableInputStored = iterableInputStored;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 原始输入是否已从热状态替换为轻量引用
|
||||
*/
|
||||
public boolean isInputExternalized() {
|
||||
return inputExternalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置原始输入外置状态。
|
||||
*
|
||||
* @param inputExternalized 是否已替换为轻量引用
|
||||
*/
|
||||
public void setInputExternalized(
|
||||
boolean inputExternalized) {
|
||||
this.inputExternalized = inputExternalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 是否正在锁外物化输入
|
||||
*/
|
||||
public boolean isMaterializingInput() {
|
||||
return materializingInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置锁外物化状态。
|
||||
*
|
||||
* @param materializingInput 是否正在物化
|
||||
*/
|
||||
public void setMaterializingInput(boolean materializingInput) {
|
||||
this.materializingInput = materializingInput;
|
||||
}
|
||||
|
||||
public int getIterableSize() {
|
||||
return iterableSize;
|
||||
}
|
||||
|
||||
public void setIterableSize(int iterableSize) {
|
||||
this.iterableSize = iterableSize;
|
||||
}
|
||||
|
||||
public int getExpectedReturnCount() {
|
||||
return expectedReturnCount;
|
||||
}
|
||||
|
||||
public void setExpectedReturnCount(int expectedReturnCount) {
|
||||
this.expectedReturnCount = expectedReturnCount;
|
||||
}
|
||||
|
||||
public Set<String> getCompletedBranchIds() {
|
||||
if (completedBranchIds == null) {
|
||||
completedBranchIds = new LinkedHashSet<>();
|
||||
}
|
||||
return completedBranchIds;
|
||||
}
|
||||
|
||||
public void setCompletedBranchIds(Set<String> completedBranchIds) {
|
||||
this.completedBranchIds = completedBranchIds;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 一次锁外循环输入物化所需的不可变守卫快照。
|
||||
*/
|
||||
private static final class MaterializationPlan {
|
||||
|
||||
private final String resultId;
|
||||
private final Iterable<?> items;
|
||||
private final String claimId;
|
||||
private final long claimGeneration;
|
||||
|
||||
/**
|
||||
* 创建物化计划。
|
||||
*
|
||||
* @param resultId 唯一结果 ID
|
||||
* @param items 输入元素
|
||||
* @param claimId 触发器 ID
|
||||
* @param claimGeneration 触发器代际
|
||||
*/
|
||||
private MaterializationPlan(
|
||||
String resultId,
|
||||
Iterable<?> items,
|
||||
String claimId,
|
||||
long claimGeneration) {
|
||||
this.resultId = resultId;
|
||||
this.items = items;
|
||||
this.claimId = claimId;
|
||||
this.claimGeneration = claimGeneration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package com.easyagents.flow.core.node;
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.ChainState;
|
||||
import com.easyagents.flow.core.searchengine.SearchEngine;
|
||||
import com.easyagents.flow.core.searchengine.SearchEngineManager;
|
||||
import com.easyagents.flow.core.util.Maps;
|
||||
@@ -29,6 +30,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class SearchEngineNode extends BaseNode {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
private static final Logger logger = org.slf4j.LoggerFactory.getLogger(SearchEngineNode.class);
|
||||
|
||||
@@ -62,11 +65,16 @@ public class SearchEngineNode extends BaseNode {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
Map<String, Object> argsMap = chain.getState().resolveParameters(this);
|
||||
ChainState chainState =
|
||||
chain.getExecutionState();
|
||||
Map<String, Object> argsMap =
|
||||
chainState.resolveParameters(this);
|
||||
List<Map<String, Object>> templateRootMaps =
|
||||
chainState.buildTemplateRootMaps(argsMap);
|
||||
String realKeyword = TextTemplate.of(keyword)
|
||||
.formatToString(chain.getState().buildTemplateRootMaps(argsMap));
|
||||
.formatToString(templateRootMaps);
|
||||
String realLimitString = TextTemplate.of(limit)
|
||||
.formatToString(chain.getState().buildTemplateRootMaps(argsMap));
|
||||
.formatToString(templateRootMaps);
|
||||
int realLimit = 10;
|
||||
if (StringUtil.hasText(realLimitString)) {
|
||||
try {
|
||||
|
||||
@@ -21,9 +21,12 @@ import com.easyagents.flow.core.chain.Chain;
|
||||
import java.util.Map;
|
||||
|
||||
public class StartNode extends BaseNode {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
return chain.getState().resolveParameters(this);
|
||||
return chain.getExecutionState()
|
||||
.resolveParameters(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -27,6 +27,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class TemplateNode extends BaseNode {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
private static final Engine engine;
|
||||
private String template;
|
||||
@@ -48,7 +50,8 @@ public class TemplateNode extends BaseNode {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
Map<String, Object> parameters = chain.getState().resolveParameters(this);
|
||||
Map<String, Object> parameters =
|
||||
chain.getExecutionState().resolveParameters(this);
|
||||
|
||||
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
||||
|
||||
|
||||
@@ -20,11 +20,13 @@ import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.chain.DataType;
|
||||
import com.easyagents.flow.core.chain.JsCodeCondition;
|
||||
import com.easyagents.flow.core.chain.Node;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.chain.RefType;
|
||||
import com.easyagents.flow.core.node.BaseNode;
|
||||
import com.easyagents.flow.core.util.StringUtil;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -156,10 +158,7 @@ public abstract class BaseNodeParser<T extends BaseNode> implements NodeParser<T
|
||||
}
|
||||
node.setLoopIntervalMs(loopIntervalMs);
|
||||
|
||||
Integer maxLoopCount = data.getInteger("maxLoopCount");
|
||||
if (maxLoopCount != null) {
|
||||
node.setMaxLoopCount(maxLoopCount);
|
||||
}
|
||||
node.setMaxLoopCount(parseMaxLoopCount(data.get("maxLoopCount")));
|
||||
|
||||
String loopBreakCondition = data.getString("loopBreakCondition");
|
||||
if (StringUtil.hasText(loopBreakCondition)) {
|
||||
@@ -201,5 +200,30 @@ public abstract class BaseNodeParser<T extends BaseNode> implements NodeParser<T
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析节点循环总次数,并拒绝小数、零值和超出上限的配置。
|
||||
*
|
||||
* @param value 原始循环次数;为空时使用默认值 1
|
||||
* @return 合法的循环总次数
|
||||
* @throws IllegalArgumentException 循环次数不是 1~300 的整数
|
||||
*/
|
||||
private int parseMaxLoopCount(Object value) {
|
||||
if (value == null) {
|
||||
return Node.MIN_LOOP_COUNT;
|
||||
}
|
||||
final int count;
|
||||
try {
|
||||
count = new BigDecimal(String.valueOf(value).trim()).intValueExact();
|
||||
} catch (ArithmeticException | NumberFormatException exception) {
|
||||
throw new IllegalArgumentException(
|
||||
"maxLoopCount must be an integer between "
|
||||
+ Node.MIN_LOOP_COUNT
|
||||
+ " and "
|
||||
+ Node.MAX_LOOP_COUNT,
|
||||
exception);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
protected abstract T doParse(JSONObject nodeJSONObject, JSONObject data, JSONObject chainJSONObject);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,489 @@
|
||||
package com.easyagents.flow.core.util;
|
||||
|
||||
import com.easyagents.flow.core.chain.runtime.RetryableTriggerException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
|
||||
/**
|
||||
* 为工作流中的阻塞 I/O 提供有界并发、单目标隔离和可观测计数。
|
||||
*/
|
||||
public final class IoBulkhead {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(IoBulkhead.class);
|
||||
|
||||
private static final String GLOBAL_CONCURRENCY_PROPERTY = "tinyflow.io.max-concurrency";
|
||||
private static final String TARGET_CONCURRENCY_PROPERTY = "tinyflow.io.per-target-max-concurrency";
|
||||
private static final String ACQUIRE_TIMEOUT_MILLIS_PROPERTY = "tinyflow.io.acquire-timeout-ms";
|
||||
private static final int DEFAULT_GLOBAL_CONCURRENCY = 64;
|
||||
private static final int DEFAULT_TARGET_CONCURRENCY = 16;
|
||||
private static final long DEFAULT_ACQUIRE_TIMEOUT_MILLIS = 1_000L;
|
||||
private static final int DEFAULT_MAX_TRACKED_TARGETS = 1_024;
|
||||
private static final long REJECTION_LOG_INTERVAL_NANOS = TimeUnit.SECONDS.toNanos(10);
|
||||
private static volatile IoBulkhead SHARED = new IoBulkhead(
|
||||
positiveIntProperty(GLOBAL_CONCURRENCY_PROPERTY, DEFAULT_GLOBAL_CONCURRENCY),
|
||||
positiveIntProperty(TARGET_CONCURRENCY_PROPERTY, DEFAULT_TARGET_CONCURRENCY),
|
||||
Duration.ofMillis(positiveLongProperty(
|
||||
ACQUIRE_TIMEOUT_MILLIS_PROPERTY,
|
||||
DEFAULT_ACQUIRE_TIMEOUT_MILLIS)),
|
||||
DEFAULT_MAX_TRACKED_TARGETS);
|
||||
private static volatile IoBulkhead DATASET = lane(
|
||||
"tinyflow.io.dataset", 32, 8, 1_000L, 512);
|
||||
private static volatile IoBulkhead STORAGE = lane(
|
||||
"tinyflow.io.storage", 24, 12, 2_000L, 256);
|
||||
private static volatile IoBulkhead DOCUMENT_PARSE = lane(
|
||||
"tinyflow.io.document-parse", 8, 4, 2_000L, 128);
|
||||
private static volatile IoBulkhead RESPONSE_AGGREGATION = lane(
|
||||
"tinyflow.io.response-aggregation", 8, 4, 2_000L, 1_024);
|
||||
|
||||
private final Semaphore globalSemaphore;
|
||||
private final int perTargetConcurrency;
|
||||
private final long acquireTimeoutNanos;
|
||||
private final int maxTrackedTargets;
|
||||
private final Map<String, Semaphore> targetSemaphores = new ConcurrentHashMap<>();
|
||||
private final Object targetRegistryLock = new Object();
|
||||
private final Semaphore overflowTargetSemaphore;
|
||||
private final LongAdder acquiredCount = new LongAdder();
|
||||
private final LongAdder rejectedCount = new LongAdder();
|
||||
private final LongAdder inFlightCount = new LongAdder();
|
||||
private final LongAdder totalWaitNanos = new LongAdder();
|
||||
private final AtomicLong lastRejectionLogNanos = new AtomicLong();
|
||||
|
||||
/**
|
||||
* 创建 I/O 隔离器。
|
||||
*
|
||||
* @param globalConcurrency 当前进程允许的 I/O 总并发
|
||||
* @param perTargetConcurrency 单个目标允许的 I/O 并发
|
||||
* @param acquireTimeout 等待许可的最长时间
|
||||
* @param maxTrackedTargets 最多单独跟踪的目标数量
|
||||
* @throws IllegalArgumentException 参数不合法时抛出
|
||||
*/
|
||||
public IoBulkhead(
|
||||
int globalConcurrency,
|
||||
int perTargetConcurrency,
|
||||
Duration acquireTimeout,
|
||||
int maxTrackedTargets) {
|
||||
if (globalConcurrency <= 0
|
||||
|| perTargetConcurrency <= 0
|
||||
|| acquireTimeout == null
|
||||
|| acquireTimeout.isNegative()
|
||||
|| maxTrackedTargets <= 0) {
|
||||
throw new IllegalArgumentException("I/O bulkhead configuration must be positive");
|
||||
}
|
||||
this.globalSemaphore = new Semaphore(globalConcurrency, true);
|
||||
this.perTargetConcurrency = perTargetConcurrency;
|
||||
this.acquireTimeoutNanos = acquireTimeout.toNanos();
|
||||
this.maxTrackedTargets = maxTrackedTargets;
|
||||
this.overflowTargetSemaphore = new Semaphore(perTargetConcurrency, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取进程内共享 I/O 隔离器。
|
||||
*
|
||||
* @return 共享隔离器
|
||||
*/
|
||||
public static IoBulkhead shared() {
|
||||
return SHARED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据集读写独立隔离器。
|
||||
*
|
||||
* @return 数据集隔离器
|
||||
*/
|
||||
public static IoBulkhead dataset() {
|
||||
return DATASET;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象存储读写独立隔离器。
|
||||
*
|
||||
* @return 对象存储隔离器
|
||||
*/
|
||||
public static IoBulkhead storage() {
|
||||
return STORAGE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文档解析独立隔离器。
|
||||
*
|
||||
* @return 文档解析隔离器
|
||||
*/
|
||||
public static IoBulkhead documentParse() {
|
||||
return DOCUMENT_PARSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取必须完整物化响应的独立小并发隔离器。
|
||||
*
|
||||
* @return 响应聚合隔离器
|
||||
*/
|
||||
public static IoBulkhead responseAggregation() {
|
||||
return RESPONSE_AGGREGATION;
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子替换全部工作流 I/O 隔离器配置。
|
||||
*
|
||||
* <p>应用应在开始处理工作流前调用。既有许可继续由旧实例释放,
|
||||
* 后续请求读取新实例,不会中断正在执行的 I/O。</p>
|
||||
*
|
||||
* @param http HTTP 请求配置
|
||||
* @param dataset 数据集配置
|
||||
* @param storage 对象存储配置
|
||||
* @param documentParse 文档解析配置
|
||||
* @param responseAggregation 响应聚合配置
|
||||
*/
|
||||
public static synchronized void configure(
|
||||
Settings http,
|
||||
Settings dataset,
|
||||
Settings storage,
|
||||
Settings documentParse,
|
||||
Settings responseAggregation) {
|
||||
SHARED = create(http);
|
||||
DATASET = create(dataset);
|
||||
STORAGE = create(storage);
|
||||
DOCUMENT_PARSE = create(documentParse);
|
||||
RESPONSE_AGGREGATION =
|
||||
create(responseAggregation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据不可变配置创建隔离器。
|
||||
*
|
||||
* @param settings 隔离配置
|
||||
* @return 新隔离器
|
||||
*/
|
||||
private static IoBulkhead create(Settings settings) {
|
||||
Settings value = java.util.Objects.requireNonNull(
|
||||
settings, "I/O bulkhead settings required");
|
||||
return new IoBulkhead(
|
||||
value.globalConcurrency(),
|
||||
value.perTargetConcurrency(),
|
||||
value.acquireTimeout(),
|
||||
value.maxTrackedTargets());
|
||||
}
|
||||
|
||||
/**
|
||||
* I/O 隔离器的不可变配置。
|
||||
*
|
||||
* @param globalConcurrency 当前进程允许的 I/O 总并发
|
||||
* @param perTargetConcurrency 单个目标允许的 I/O 并发
|
||||
* @param acquireTimeout 等待许可的最长时间
|
||||
* @param maxTrackedTargets 最多单独跟踪的目标数量
|
||||
*/
|
||||
public record Settings(
|
||||
int globalConcurrency,
|
||||
int perTargetConcurrency,
|
||||
Duration acquireTimeout,
|
||||
int maxTrackedTargets) {
|
||||
|
||||
/**
|
||||
* 校验配置,避免应用启动后才暴露无效容量。
|
||||
*
|
||||
* @throws IllegalArgumentException 配置值无效时抛出
|
||||
*/
|
||||
public Settings {
|
||||
if (globalConcurrency <= 0
|
||||
|| perTargetConcurrency <= 0
|
||||
|| acquireTimeout == null
|
||||
|| acquireTimeout.isNegative()
|
||||
|| acquireTimeout.isZero()
|
||||
|| maxTrackedTargets <= 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"I/O bulkhead settings must be positive");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 URL 转换为稳定的协议和主机目标键。
|
||||
*
|
||||
* @param url 请求 URL
|
||||
* @return 目标键;URL 不合法时返回通用 HTTP 目标
|
||||
*/
|
||||
public static String targetForUrl(String url) {
|
||||
if (!StringUtil.hasText(url)) {
|
||||
return "http:unknown";
|
||||
}
|
||||
try {
|
||||
URI uri = URI.create(url.trim());
|
||||
String host = uri.getHost();
|
||||
if (!StringUtil.hasText(host)) {
|
||||
return "http:unknown";
|
||||
}
|
||||
int port = uri.getPort();
|
||||
return "http:"
|
||||
+ host.toLowerCase(Locale.ROOT)
|
||||
+ (port < 0 ? "" : ":" + port);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
return "http:unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在给定目标上申请一次阻塞 I/O 许可。
|
||||
*
|
||||
* @param target 目标标识
|
||||
* @return 使用完成后必须关闭的许可
|
||||
* @throws RetryableTriggerException 等待超时或线程被中断时抛出
|
||||
*/
|
||||
public Permit acquire(String target) {
|
||||
String normalizedTarget = normalizeTarget(target);
|
||||
Semaphore targetSemaphore = targetSemaphore(normalizedTarget);
|
||||
long startedAt = System.nanoTime();
|
||||
boolean targetAcquired = false;
|
||||
boolean globalAcquired = false;
|
||||
try {
|
||||
targetAcquired = targetSemaphore.tryAcquire(acquireTimeoutNanos, TimeUnit.NANOSECONDS);
|
||||
if (!targetAcquired) {
|
||||
throw rejection(normalizedTarget, startedAt, null);
|
||||
}
|
||||
long remainingNanos = Math.max(
|
||||
0L,
|
||||
acquireTimeoutNanos - (System.nanoTime() - startedAt));
|
||||
globalAcquired = globalSemaphore.tryAcquire(remainingNanos, TimeUnit.NANOSECONDS);
|
||||
if (!globalAcquired) {
|
||||
throw rejection(normalizedTarget, startedAt, null);
|
||||
}
|
||||
totalWaitNanos.add(System.nanoTime() - startedAt);
|
||||
acquiredCount.increment();
|
||||
inFlightCount.increment();
|
||||
return new Permit(this, targetSemaphore);
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw rejection(normalizedTarget, startedAt, exception);
|
||||
} finally {
|
||||
if (!globalAcquired && targetAcquired) {
|
||||
targetSemaphore.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前隔离器的轻量运行指标。
|
||||
*
|
||||
* @return 指标快照
|
||||
*/
|
||||
public Snapshot snapshot() {
|
||||
return new Snapshot(
|
||||
acquiredCount.sum(),
|
||||
rejectedCount.sum(),
|
||||
inFlightCount.sum(),
|
||||
totalWaitNanos.sum(),
|
||||
globalSemaphore.availablePermits(),
|
||||
targetSemaphores.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放成功申请的全局和目标许可。
|
||||
*
|
||||
* @param targetSemaphore 已申请的目标信号量
|
||||
*/
|
||||
private void release(Semaphore targetSemaphore) {
|
||||
inFlightCount.decrement();
|
||||
globalSemaphore.release();
|
||||
targetSemaphore.release();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目标对应的信号量,并限制目标注册表体积。
|
||||
*
|
||||
* @param target 规范化目标
|
||||
* @return 目标信号量
|
||||
*/
|
||||
private Semaphore targetSemaphore(String target) {
|
||||
Semaphore existing = targetSemaphores.get(target);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
synchronized (targetRegistryLock) {
|
||||
existing = targetSemaphores.get(target);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
if (targetSemaphores.size() >= maxTrackedTargets) {
|
||||
return overflowTargetSemaphore;
|
||||
}
|
||||
Semaphore created = new Semaphore(perTargetConcurrency, true);
|
||||
targetSemaphores.put(target, created);
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造可重新投递的限流异常并记录指标。
|
||||
*
|
||||
* @param target 目标标识
|
||||
* @param startedAt 等待开始时间
|
||||
* @param cause 原始异常
|
||||
* @return 可重新投递异常
|
||||
*/
|
||||
private RetryableTriggerException rejection(String target, long startedAt, Throwable cause) {
|
||||
long waitedNanos = System.nanoTime() - startedAt;
|
||||
totalWaitNanos.add(waitedNanos);
|
||||
rejectedCount.increment();
|
||||
TimeoutException timeoutException = new TimeoutException(
|
||||
"I/O bulkhead is saturated for target " + target);
|
||||
if (cause != null) {
|
||||
timeoutException.initCause(cause);
|
||||
}
|
||||
logRejectionIfDue(target, waitedNanos);
|
||||
return new RetryableTriggerException("I/O 资源繁忙,请稍后重试", timeoutException);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对饱和日志限频,避免过载期间放大日志 I/O。
|
||||
*
|
||||
* @param target 目标标识
|
||||
* @param waitedNanos 本次等待纳秒数
|
||||
*/
|
||||
private void logRejectionIfDue(String target, long waitedNanos) {
|
||||
long now = System.nanoTime();
|
||||
long previous = lastRejectionLogNanos.get();
|
||||
if ((previous != 0L && now - previous < REJECTION_LOG_INTERVAL_NANOS)
|
||||
|| !lastRejectionLogNanos.compareAndSet(previous, now)) {
|
||||
return;
|
||||
}
|
||||
log.warn(
|
||||
"工作流 I/O 隔离器拒绝请求,target={}, waitedMs={}, inFlight={}, rejected={}",
|
||||
target,
|
||||
TimeUnit.NANOSECONDS.toMillis(waitedNanos),
|
||||
inFlightCount.sum(),
|
||||
rejectedCount.sum());
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化目标键。
|
||||
*
|
||||
* @param target 原始目标
|
||||
* @return 非空目标键
|
||||
*/
|
||||
private static String normalizeTarget(String target) {
|
||||
return StringUtil.hasText(target) ? target.trim() : "io:unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取正整数系统属性。
|
||||
*
|
||||
* @param name 属性名
|
||||
* @param defaultValue 默认值
|
||||
* @return 有效属性值或默认值
|
||||
*/
|
||||
private static int positiveIntProperty(String name, int defaultValue) {
|
||||
try {
|
||||
int value = Integer.parseInt(System.getProperty(name, String.valueOf(defaultValue)));
|
||||
return value > 0 ? value : defaultValue;
|
||||
} catch (NumberFormatException exception) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取正长整数系统属性。
|
||||
*
|
||||
* @param name 属性名
|
||||
* @param defaultValue 默认值
|
||||
* @return 有效属性值或默认值
|
||||
*/
|
||||
private static long positiveLongProperty(String name, long defaultValue) {
|
||||
try {
|
||||
long value = Long.parseLong(System.getProperty(name, String.valueOf(defaultValue)));
|
||||
return value > 0L ? value : defaultValue;
|
||||
} catch (NumberFormatException exception) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按资源类别创建具有独立容量的隔离器。
|
||||
*
|
||||
* @param prefix 系统属性前缀
|
||||
* @param globalConcurrency 默认总并发
|
||||
* @param targetConcurrency 默认单目标并发
|
||||
* @param timeoutMillis 默认等待毫秒数
|
||||
* @param trackedTargets 默认目标上限
|
||||
* @return 独立隔离器
|
||||
*/
|
||||
private static IoBulkhead lane(
|
||||
String prefix,
|
||||
int globalConcurrency,
|
||||
int targetConcurrency,
|
||||
long timeoutMillis,
|
||||
int trackedTargets) {
|
||||
return new IoBulkhead(
|
||||
positiveIntProperty(
|
||||
prefix + ".max-concurrency",
|
||||
globalConcurrency),
|
||||
positiveIntProperty(
|
||||
prefix + ".per-target-max-concurrency",
|
||||
targetConcurrency),
|
||||
Duration.ofMillis(positiveLongProperty(
|
||||
prefix + ".acquire-timeout-ms",
|
||||
timeoutMillis)),
|
||||
positiveIntProperty(
|
||||
prefix + ".max-tracked-targets",
|
||||
trackedTargets));
|
||||
}
|
||||
|
||||
/**
|
||||
* 一次 I/O 许可,关闭时幂等释放资源。
|
||||
*/
|
||||
public static final class Permit implements AutoCloseable {
|
||||
|
||||
private final IoBulkhead owner;
|
||||
private final Semaphore targetSemaphore;
|
||||
private final AtomicBoolean closed = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
* 创建许可。
|
||||
*
|
||||
* @param owner 所属隔离器
|
||||
* @param targetSemaphore 目标信号量
|
||||
*/
|
||||
private Permit(IoBulkhead owner, Semaphore targetSemaphore) {
|
||||
this.owner = owner;
|
||||
this.targetSemaphore = targetSemaphore;
|
||||
}
|
||||
|
||||
/**
|
||||
* 幂等释放许可。
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
if (closed.compareAndSet(false, true)) {
|
||||
owner.release(targetSemaphore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* I/O 隔离器的不可变运行指标快照。
|
||||
*
|
||||
* @param acquiredCount 已获取许可次数
|
||||
* @param rejectedCount 被拒绝次数
|
||||
* @param inFlightCount 当前执行中数量
|
||||
* @param totalWaitNanos 累计等待纳秒数
|
||||
* @param availableGlobalPermits 当前可用全局许可数
|
||||
* @param trackedTargetCount 当前独立跟踪目标数
|
||||
*/
|
||||
public record Snapshot(
|
||||
long acquiredCount,
|
||||
long rejectedCount,
|
||||
long inFlightCount,
|
||||
long totalWaitNanos,
|
||||
int availableGlobalPermits,
|
||||
int trackedTargetCount) {
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.easyagents.flow.core.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
@@ -61,4 +62,25 @@ public class IterableUtil {
|
||||
|
||||
throw new IndexOutOfBoundsException("index >= size: " + index);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单次遍历并物化 Iterable,避免后续按索引从头重复扫描。
|
||||
*
|
||||
* @param iterable 可迭代对象
|
||||
* @param <T> 元素类型
|
||||
* @return 保持原始迭代顺序的列表
|
||||
*/
|
||||
public static <T> List<T> toList(Iterable<T> iterable) {
|
||||
if (iterable == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
if (iterable instanceof Collection) {
|
||||
return new ArrayList<>((Collection<T>) iterable);
|
||||
}
|
||||
List<T> result = new ArrayList<>();
|
||||
for (T item : iterable) {
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,21 +18,84 @@ package com.easyagents.flow.core.util;
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.util.graalvm.JsInteropUtils;
|
||||
import org.graalvm.polyglot.Context;
|
||||
import org.graalvm.polyglot.Engine;
|
||||
import org.graalvm.polyglot.HostAccess;
|
||||
import org.graalvm.polyglot.Source;
|
||||
import org.graalvm.polyglot.Value;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 在隔离的 GraalVM JavaScript 上下文中执行工作流条件表达式。
|
||||
*/
|
||||
public class JsConditionUtil {
|
||||
|
||||
// 使用 Context.Builder 构建上下文,线程安全
|
||||
private static final Context.Builder CONTEXT_BUILDER = Context.newBuilder("js")
|
||||
private static final int SOURCE_CACHE_LIMIT = 2048;
|
||||
/**
|
||||
* 单条超长动态表达式不进入共享缓存,限制源码字符串和编译元数据总占用。
|
||||
*/
|
||||
private static final int MAX_CACHEABLE_SOURCE_CHARS =
|
||||
16 * 1024;
|
||||
/**
|
||||
* Engine 跨 Context 共享编译缓存,Context 仍按每次求值独立创建。
|
||||
*/
|
||||
private static final Engine ENGINE = Engine.newBuilder()
|
||||
.option("engine.WarnInterpreterOnly", "false")
|
||||
.build();
|
||||
private static final Map<String, Source> SOURCE_CACHE =
|
||||
Collections.synchronizedMap(new LinkedHashMap<>(
|
||||
SOURCE_CACHE_LIMIT + 1, 0.75F, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(
|
||||
Map.Entry<String, Source> eldest) {
|
||||
return size() > SOURCE_CACHE_LIMIT;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 工具类禁止实例化。
|
||||
*/
|
||||
private JsConditionUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一次隔离的 JavaScript 执行上下文。
|
||||
*
|
||||
* @return 新的执行上下文
|
||||
*/
|
||||
private static Context createContext() {
|
||||
return Context.newBuilder("js")
|
||||
.engine(ENGINE)
|
||||
.allowHostAccess(HostAccess.ALL) // 允许访问 Java 对象的方法和字段
|
||||
.allowHostClassLookup(className -> false) // 禁止动态加载任意 Java 类
|
||||
.option("js.ecmascript-version", "2021"); // 使用较新的 ECMAScript 版本
|
||||
.option("js.ecmascript-version", "2021")
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取复用的条件表达式源对象。
|
||||
*
|
||||
* @param code 原始表达式
|
||||
* @return 包含结果赋值逻辑的 JavaScript 源
|
||||
*/
|
||||
private static Source source(String code) {
|
||||
if (code.length()
|
||||
> MAX_CACHEABLE_SOURCE_CHARS) {
|
||||
return Source.create(
|
||||
"js",
|
||||
"_result.value = " + code);
|
||||
}
|
||||
synchronized (SOURCE_CACHE) {
|
||||
return SOURCE_CACHE.computeIfAbsent(
|
||||
code,
|
||||
expression -> Source.create(
|
||||
"js",
|
||||
"_result.value = " + expression));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 JavaScript 表达式并返回 boolean 结果
|
||||
@@ -43,7 +106,7 @@ public class JsConditionUtil {
|
||||
* @return true 表示满足条件,继续执行;false 表示跳过
|
||||
*/
|
||||
public static boolean eval(String code, Chain chain, Map<String, Object> initMap) {
|
||||
try (Context context = CONTEXT_BUILDER.build()) {
|
||||
try (Context context = createContext()) {
|
||||
Map<String, Object> _result = new HashMap<>();
|
||||
Value bindings = context.getBindings("js");
|
||||
|
||||
@@ -54,9 +117,8 @@ public class JsConditionUtil {
|
||||
});
|
||||
|
||||
bindings.putMember("_result", _result);
|
||||
code = "_result.value = " + code;
|
||||
|
||||
context.eval("js", code);
|
||||
context.eval(source(code));
|
||||
Object value = _result.get("value");
|
||||
return toBoolean(value);
|
||||
} catch (Exception e) {
|
||||
@@ -65,8 +127,16 @@ public class JsConditionUtil {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 执行 JavaScript 表达式并转换为长整数。
|
||||
*
|
||||
* @param code JS 表达式
|
||||
* @param chain Chain 上下文对象
|
||||
* @param initMap 初始变量映射
|
||||
* @return 转换后的长整数
|
||||
*/
|
||||
public static long evalLong(String code, Chain chain, Map<String, Object> initMap) {
|
||||
try (Context context = CONTEXT_BUILDER.build()) {
|
||||
try (Context context = createContext()) {
|
||||
Map<String, Object> _result = new HashMap<>();
|
||||
Value bindings = context.getBindings("js");
|
||||
|
||||
@@ -77,9 +147,8 @@ public class JsConditionUtil {
|
||||
});
|
||||
|
||||
bindings.putMember("_result", _result);
|
||||
code = "_result.value = " + code;
|
||||
|
||||
context.eval("js", code);
|
||||
context.eval(source(code));
|
||||
Object value = _result.get("value");
|
||||
return toLong(value);
|
||||
} catch (Exception e) {
|
||||
@@ -89,7 +158,10 @@ public class JsConditionUtil {
|
||||
|
||||
|
||||
/**
|
||||
* 将任意对象安全转换为 long 类型
|
||||
* 将任意对象安全转换为 long 类型。
|
||||
*
|
||||
* @param value 待转换值
|
||||
* @return 长整数结果
|
||||
*/
|
||||
private static long toLong(Object value) {
|
||||
if (value == null) {
|
||||
@@ -136,13 +208,17 @@ public class JsConditionUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集上下文中的变量
|
||||
* 收集上下文中的变量。
|
||||
*
|
||||
* @param chain 当前工作流
|
||||
* @param initMap 初始变量
|
||||
* @return JavaScript 变量映射
|
||||
*/
|
||||
private static Map<String, Object> collectContextVariables(Chain chain, Map<String, Object> initMap) {
|
||||
Map<String, Object> variables = new ConcurrentHashMap<>();
|
||||
Map<String, Object> variables = new HashMap<>();
|
||||
|
||||
// 添加 Chain Memory 中的变量(去掉前缀)
|
||||
chain.getState().getMemory().forEach((key, value) -> {
|
||||
chain.getExecutionState().getMemory().forEach((key, value) -> {
|
||||
int dotIndex = key.indexOf(".");
|
||||
String varName = (dotIndex >= 0) ? key.substring(dotIndex + 1) : key;
|
||||
variables.put(varName, value);
|
||||
@@ -155,7 +231,10 @@ public class JsConditionUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* 将任意对象转换为布尔值
|
||||
* 将任意对象转换为布尔值。
|
||||
*
|
||||
* @param value 待转换值
|
||||
* @return 布尔结果
|
||||
*/
|
||||
private static boolean toBoolean(Object value) {
|
||||
if (value == null) {
|
||||
|
||||
@@ -16,6 +16,13 @@
|
||||
package com.easyagents.flow.core.util;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import okio.BufferedSource;
|
||||
import okio.ForwardingSource;
|
||||
import okio.Okio;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
@@ -43,6 +50,8 @@ public final class OkHttpClientUtil {
|
||||
private static final Logger LOGGER = Logger.getLogger(OkHttpClientUtil.class.getName());
|
||||
|
||||
private static volatile OkHttpClient.Builder customBuilder;
|
||||
private static volatile OkHttpClient sharedClient;
|
||||
private static volatile OkHttpClient sharedNoRetryClient;
|
||||
private static final Object LOCK = new Object();
|
||||
|
||||
// Prevent instantiation
|
||||
@@ -58,7 +67,11 @@ public final class OkHttpClientUtil {
|
||||
if (builder == null) {
|
||||
throw new IllegalArgumentException("Builder must not be null");
|
||||
}
|
||||
synchronized (LOCK) {
|
||||
customBuilder = builder;
|
||||
sharedClient = null;
|
||||
sharedNoRetryClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,18 +83,19 @@ public final class OkHttpClientUtil {
|
||||
* </p>
|
||||
*/
|
||||
public static OkHttpClient buildDefaultClient() {
|
||||
OkHttpClient.Builder builder = customBuilder;
|
||||
if (builder != null) {
|
||||
return builder.build();
|
||||
OkHttpClient client = sharedClient;
|
||||
if (client != null) {
|
||||
return client;
|
||||
}
|
||||
|
||||
synchronized (LOCK) {
|
||||
// Double-check in case another thread set it while waiting
|
||||
builder = customBuilder;
|
||||
if (builder != null) {
|
||||
return builder.build();
|
||||
client = sharedClient;
|
||||
if (client != null) {
|
||||
return client;
|
||||
}
|
||||
|
||||
OkHttpClient.Builder builder = customBuilder;
|
||||
if (builder == null) {
|
||||
builder = new OkHttpClient.Builder()
|
||||
.connectTimeout(1, TimeUnit.MINUTES)
|
||||
.readTimeout(5, TimeUnit.MINUTES);
|
||||
@@ -94,7 +108,48 @@ public final class OkHttpClientUtil {
|
||||
}
|
||||
|
||||
configureProxy(builder);
|
||||
return builder.build();
|
||||
}
|
||||
configureIoBulkhead(builder);
|
||||
sharedClient = builder.build();
|
||||
return sharedClient;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回关闭 OkHttp 隐式连接重试的共享客户端。
|
||||
*
|
||||
* <p>该客户端与默认客户端复用连接池和调度器,用于可能产生业务副作用的请求。</p>
|
||||
*
|
||||
* @return 禁用连接失败自动重试的共享客户端
|
||||
*/
|
||||
public static OkHttpClient buildNoRetryClient() {
|
||||
OkHttpClient client = sharedNoRetryClient;
|
||||
if (client != null) {
|
||||
return client;
|
||||
}
|
||||
synchronized (LOCK) {
|
||||
client = sharedNoRetryClient;
|
||||
if (client == null) {
|
||||
client = buildDefaultClient()
|
||||
.newBuilder()
|
||||
.retryOnConnectionFailure(false)
|
||||
.build();
|
||||
sharedNoRetryClient = client;
|
||||
}
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为共享客户端安装一次工作流 I/O 隔离拦截器。
|
||||
*
|
||||
* @param builder HTTP 客户端构建器
|
||||
*/
|
||||
private static void configureIoBulkhead(OkHttpClient.Builder builder) {
|
||||
boolean configured = builder.interceptors().stream()
|
||||
.anyMatch(interceptor -> interceptor instanceof IoBulkheadInterceptor);
|
||||
if (!configured) {
|
||||
builder.addInterceptor(new IoBulkheadInterceptor());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,4 +217,99 @@ public final class OkHttpClientUtil {
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 HTTP 响应体关闭前持有 I/O 许可的拦截器。
|
||||
*/
|
||||
private static final class IoBulkheadInterceptor implements Interceptor {
|
||||
|
||||
/**
|
||||
* 对单个 HTTP 目标施加并发隔离。
|
||||
*
|
||||
* @param chain OkHttp 拦截链
|
||||
* @return HTTP 响应
|
||||
* @throws java.io.IOException 网络请求失败时抛出
|
||||
*/
|
||||
@Override
|
||||
public Response intercept(Chain chain) throws java.io.IOException {
|
||||
IoBulkhead.Permit permit = IoBulkhead.shared()
|
||||
.acquire(IoBulkhead.targetForUrl(chain.request().url().toString()));
|
||||
boolean transferred = false;
|
||||
try {
|
||||
Response response = chain.proceed(chain.request());
|
||||
ResponseBody body = response.body();
|
||||
if (body == null) {
|
||||
return response;
|
||||
}
|
||||
Response wrapped = response.newBuilder()
|
||||
.body(new PermitReleasingResponseBody(body, permit))
|
||||
.build();
|
||||
transferred = true;
|
||||
return wrapped;
|
||||
} finally {
|
||||
if (!transferred) {
|
||||
permit.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在响应体关闭时释放 I/O 许可的响应体包装器。
|
||||
*/
|
||||
private static final class PermitReleasingResponseBody extends ResponseBody {
|
||||
|
||||
private final ResponseBody delegate;
|
||||
private final BufferedSource source;
|
||||
|
||||
/**
|
||||
* 创建响应体包装器。
|
||||
*
|
||||
* @param delegate 原始响应体
|
||||
* @param permit 待释放的 I/O 许可
|
||||
*/
|
||||
private PermitReleasingResponseBody(ResponseBody delegate, IoBulkhead.Permit permit) {
|
||||
this.delegate = delegate;
|
||||
this.source = Okio.buffer(new ForwardingSource(delegate.source()) {
|
||||
@Override
|
||||
public void close() throws java.io.IOException {
|
||||
try {
|
||||
super.close();
|
||||
} finally {
|
||||
permit.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取响应媒体类型。
|
||||
*
|
||||
* @return 响应媒体类型
|
||||
*/
|
||||
@Override
|
||||
public MediaType contentType() {
|
||||
return delegate.contentType();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取响应声明长度。
|
||||
*
|
||||
* @return 响应声明长度
|
||||
*/
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return delegate.contentLength();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取带许可释放逻辑的响应源。
|
||||
*
|
||||
* @return 响应源
|
||||
*/
|
||||
@Override
|
||||
public BufferedSource source() {
|
||||
return source;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,12 +46,16 @@ public class TextTemplate {
|
||||
/**
|
||||
* 模板缓存(按原始模板字符串)
|
||||
*/
|
||||
private static final Map<String, TextTemplate> TEMPLATE_CACHE = new ConcurrentHashMap<>();
|
||||
private static final int TEMPLATE_CACHE_LIMIT = 4096;
|
||||
private static final int JSONPATH_CACHE_LIMIT = 2048;
|
||||
private static final Map<String, TextTemplate> TEMPLATE_CACHE =
|
||||
Collections.synchronizedMap(new BoundedLruMap<>(TEMPLATE_CACHE_LIMIT));
|
||||
|
||||
/**
|
||||
* JSONPath 编译缓存,避免重复编译
|
||||
*/
|
||||
private static final Map<String, JSONPath> JSONPATH_CACHE = new ConcurrentHashMap<>();
|
||||
private static final Map<String, JSONPath> JSONPATH_CACHE =
|
||||
Collections.synchronizedMap(new BoundedLruMap<>(JSONPATH_CACHE_LIMIT));
|
||||
|
||||
/**
|
||||
* 原始模板字符串
|
||||
@@ -73,7 +77,9 @@ public class TextTemplate {
|
||||
*/
|
||||
public static TextTemplate of(String template) {
|
||||
String finalTemplate = template != null ? template : "";
|
||||
return MapUtil.computeIfAbsent(TEMPLATE_CACHE, finalTemplate, k -> new TextTemplate(finalTemplate));
|
||||
synchronized (TEMPLATE_CACHE) {
|
||||
return TEMPLATE_CACHE.computeIfAbsent(finalTemplate, TextTemplate::new);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,13 +92,42 @@ public class TextTemplate {
|
||||
|
||||
|
||||
public String formatToString(List<Map<String, Object>> rootMaps) {
|
||||
Map<String, Object> rootMap = new HashMap<>();
|
||||
for (Map<String, Object> m : rootMaps) {
|
||||
if (m != null) {
|
||||
rootMap.putAll(m);
|
||||
return formatToString(rootMaps, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用分层上下文格式化模板,避免合并复制完整工作流状态。
|
||||
* <p>
|
||||
* 后面的上下文层优先级更高,与原有 Map 合并顺序一致。
|
||||
*
|
||||
* @param rootMaps 分层模板上下文
|
||||
* @param escapeForJsonOutput 是否对结果进行 JSON 字符串转义
|
||||
* @return 格式化结果
|
||||
*/
|
||||
public String formatToString(List<Map<String, Object>> rootMaps, boolean escapeForJsonOutput) {
|
||||
if (tokens.isEmpty()) {
|
||||
return originalTemplate;
|
||||
}
|
||||
return formatToString(rootMap, false);
|
||||
List<Map<String, Object>> contexts = rootMaps == null ? Collections.emptyList() : rootMaps;
|
||||
Map<String, Object> layeredContext = new LayeredContextMap(contexts);
|
||||
StringBuilder result = new StringBuilder(originalTemplate.length() + 64);
|
||||
for (TemplateToken token : tokens) {
|
||||
if (token.isStatic) {
|
||||
result.append(token.content);
|
||||
continue;
|
||||
}
|
||||
EvaluationResult evaluationResult = evaluate(
|
||||
token.parseResult, layeredContext, escapeForJsonOutput);
|
||||
if (!token.explicitEmptyFallback && !evaluationResult.resolved) {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Missing value for expression: \"%s\"%nTemplate: %s%nProvided context layers: %d",
|
||||
token.rawExpression,
|
||||
originalTemplate,
|
||||
contexts.size()));
|
||||
}
|
||||
result.append(evaluationResult.value);
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,7 +271,10 @@ public class TextTemplate {
|
||||
}
|
||||
|
||||
String fullPath = path.startsWith("$") ? path : "$." + path;
|
||||
JSONPath compiled = MapUtil.computeIfAbsent(JSONPATH_CACHE, fullPath, JSONPath::compile);
|
||||
JSONPath compiled;
|
||||
synchronized (JSONPATH_CACHE) {
|
||||
compiled = JSONPATH_CACHE.computeIfAbsent(fullPath, JSONPath::compile);
|
||||
}
|
||||
Object value = compiled.eval(root);
|
||||
if (escapeForJsonOutput && value instanceof String) {
|
||||
return escapeJsonString((String) value);
|
||||
@@ -384,4 +422,159 @@ public class TextTemplate {
|
||||
return new EvaluationResult(false, "");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保持 putAll 覆盖语义、但不复制底层数据的只读分层 Map。
|
||||
*/
|
||||
private static final class LayeredContextMap extends AbstractMap<String, Object> {
|
||||
|
||||
private final List<Map<String, Object>> layers;
|
||||
|
||||
private LayeredContextMap(List<Map<String, Object>> layers) {
|
||||
this.layers = layers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get(Object key) {
|
||||
for (int index = layers.size() - 1; index >= 0; index--) {
|
||||
Map<String, Object> layer = layers.get(index);
|
||||
if (layer != null && layer.containsKey(key)) {
|
||||
return layer.get(key);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
for (int index = layers.size() - 1; index >= 0; index--) {
|
||||
Map<String, Object> layer = layers.get(index);
|
||||
if (layer != null && layer.containsKey(key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
for (Map<String, Object> layer : layers) {
|
||||
if (layer != null && !layer.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return keySet().size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> keySet() {
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
for (Map<String, Object> layer : layers) {
|
||||
if (layer != null) {
|
||||
keys.addAll(layer.keySet());
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableSet(keys);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Entry<String, Object>> entrySet() {
|
||||
Set<String> keys = keySet();
|
||||
return new AbstractSet<>() {
|
||||
@Override
|
||||
public Iterator<Entry<String, Object>> iterator() {
|
||||
Iterator<String> iterator =
|
||||
keys.iterator();
|
||||
return new Iterator<>() {
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return iterator.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entry<String, Object> next() {
|
||||
String key = iterator.next();
|
||||
return lazyEntry(key);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return keys.size();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建仅在读取值时访问底层上下文的不可变条目。
|
||||
*
|
||||
* @param key 上下文键
|
||||
* @return 惰性条目
|
||||
*/
|
||||
private Entry<String, Object> lazyEntry(
|
||||
String key) {
|
||||
return new Entry<>() {
|
||||
@Override
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return LayeredContextMap.this
|
||||
.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object setValue(Object value) {
|
||||
throw new UnsupportedOperationException(
|
||||
"read-only template context");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object value) {
|
||||
return value instanceof Entry<?, ?> entry
|
||||
&& Objects.equals(
|
||||
key, entry.getKey())
|
||||
&& Objects.equals(
|
||||
getValue(),
|
||||
entry.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(key)
|
||||
^ Objects.hashCode(
|
||||
getValue());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 固定容量的最近最少使用缓存。
|
||||
*
|
||||
* @param <K> 键类型
|
||||
* @param <V> 值类型
|
||||
*/
|
||||
private static final class BoundedLruMap<K, V> extends LinkedHashMap<K, V> {
|
||||
|
||||
private final int limit;
|
||||
|
||||
private BoundedLruMap(int limit) {
|
||||
super(16, 0.75F, true);
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
|
||||
return size() > limit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.easyagents.flow.core.node;
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.ResponseBody;
|
||||
import okio.Buffer;
|
||||
import okio.BufferedSource;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link HttpNode} 性能保护与重试安全回归测试。
|
||||
*/
|
||||
public class HttpNodePerformanceSafetyTest {
|
||||
|
||||
/**
|
||||
* 验证非幂等请求不会执行节点内部隐式自动重试。
|
||||
*/
|
||||
@Test
|
||||
public void shouldNotAutomaticallyRetryNonIdempotentMethod() {
|
||||
FailingHttpNode node = new FailingHttpNode();
|
||||
node.setMethod("POST");
|
||||
|
||||
try {
|
||||
node.execute(null);
|
||||
Assert.fail("request should fail");
|
||||
} catch (RuntimeException expected) {
|
||||
Assert.assertEquals(1, node.attempts);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证仅读取类 HTTP 方法允许节点内部自动重试。
|
||||
*/
|
||||
@Test
|
||||
public void shouldOnlyRetrySafeReadMethods() {
|
||||
FailingHttpNode node = new FailingHttpNode();
|
||||
|
||||
Assert.assertTrue(node.allowsAutomaticRetry("GET"));
|
||||
Assert.assertTrue(node.allowsAutomaticRetry("HEAD"));
|
||||
Assert.assertTrue(node.allowsAutomaticRetry("OPTIONS"));
|
||||
Assert.assertFalse(node.allowsAutomaticRetry("POST"));
|
||||
Assert.assertFalse(node.allowsAutomaticRetry("PUT"));
|
||||
Assert.assertFalse(node.allowsAutomaticRetry("PATCH"));
|
||||
Assert.assertFalse(node.allowsAutomaticRetry("DELETE"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证响应头未声明长度时仍按实际读取量拒绝超限文本。
|
||||
*
|
||||
* @throws Exception 响应读取失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldEnforceActualTextResponseSize() throws Exception {
|
||||
FailingHttpNode node = new FailingHttpNode();
|
||||
ResponseBody body = new ResponseBody() {
|
||||
@Override
|
||||
public MediaType contentType() {
|
||||
return MediaType.parse("text/plain; charset=utf-8");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return -1L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedSource source() {
|
||||
return new Buffer().writeUtf8("12345");
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
node.read(body, 4L);
|
||||
Assert.fail("actual response bytes should be limited");
|
||||
} catch (IOException exception) {
|
||||
Assert.assertTrue(exception.getMessage().contains("4 bytes"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 始终返回 I/O 异常的测试节点。
|
||||
*/
|
||||
private static final class FailingHttpNode extends HttpNode {
|
||||
|
||||
private int attempts;
|
||||
|
||||
/**
|
||||
* 模拟单次 HTTP 调用失败。
|
||||
*
|
||||
* @param chain 工作流链
|
||||
* @return 不会正常返回
|
||||
* @throws IOException 固定抛出模拟异常
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> doExecute(Chain chain) throws IOException {
|
||||
attempts++;
|
||||
throw new IOException("simulated failure");
|
||||
}
|
||||
|
||||
/**
|
||||
* 暴露自动重试判断供测试使用。
|
||||
*
|
||||
* @param method HTTP 方法
|
||||
* @return 是否允许自动重试
|
||||
*/
|
||||
private boolean allowsAutomaticRetry(String method) {
|
||||
return supportsAutomaticRetry(method);
|
||||
}
|
||||
|
||||
/**
|
||||
* 暴露文本响应读取供测试使用。
|
||||
*
|
||||
* @param body 响应体
|
||||
* @param maxBytes 最大允许字节数
|
||||
* @return 响应文本
|
||||
* @throws IOException 读取失败或超限时抛出
|
||||
*/
|
||||
private String read(ResponseBody body, long maxBytes) throws IOException {
|
||||
return readTextBody(body, maxBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package com.easyagents.flow.core.test;
|
||||
|
||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
import com.easyagents.flow.core.chain.Edge;
|
||||
import com.easyagents.flow.core.node.EndNode;
|
||||
import com.easyagents.flow.core.node.StartNode;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* 验证工作流定义图索引的查询结果和变更失效行为。
|
||||
*/
|
||||
public class ChainDefinitionIndexTest {
|
||||
|
||||
/**
|
||||
* 验证节点、边、邻接表和开始节点保持原有查询语义。
|
||||
*/
|
||||
@Test
|
||||
public void shouldQueryDefinitionThroughGraphIndex() {
|
||||
ChainDefinition definition = new ChainDefinition();
|
||||
StartNode startNode = node(new StartNode(), "start");
|
||||
EndNode endNode = node(new EndNode(), "end");
|
||||
Edge edge = edge("start-to-end", "start", "end");
|
||||
|
||||
definition.addNode(startNode);
|
||||
definition.addNode(endNode);
|
||||
definition.addEdge(edge);
|
||||
|
||||
Assert.assertSame(startNode, definition.getNodeById("start"));
|
||||
Assert.assertSame(edge, definition.getEdgeById("start-to-end"));
|
||||
Assert.assertEquals(Collections.singletonList(edge), definition.getOutwardEdge("start"));
|
||||
Assert.assertEquals(Collections.singletonList(edge), definition.getInwardEdge("end"));
|
||||
Assert.assertEquals(Collections.singletonList(startNode), definition.getStartNodes());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证定义修改后会重建图索引,不返回过期结果。
|
||||
*/
|
||||
@Test
|
||||
public void shouldInvalidateGraphIndexAfterDefinitionChanges() {
|
||||
ChainDefinition definition = new ChainDefinition();
|
||||
StartNode startNode = node(new StartNode(), "start");
|
||||
EndNode firstEnd = node(new EndNode(), "end-1");
|
||||
definition.addNode(startNode);
|
||||
definition.addNode(firstEnd);
|
||||
definition.addEdge(edge("edge-1", "start", "end-1"));
|
||||
|
||||
Assert.assertEquals(1, definition.getOutwardEdge("start").size());
|
||||
|
||||
EndNode secondEnd = node(new EndNode(), "end-2");
|
||||
definition.addNode(secondEnd);
|
||||
definition.addEdge(edge("edge-2", "start", "end-2"));
|
||||
|
||||
Assert.assertSame(secondEnd, definition.getNodeById("end-2"));
|
||||
Assert.assertEquals(2, definition.getOutwardEdge("start").size());
|
||||
Assert.assertEquals(Collections.singletonList(startNode), definition.getStartNodes());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证包含节点、边和条件的定义可作为实例快照完整序列化。
|
||||
*
|
||||
* @throws Exception 序列化或反序列化失败
|
||||
*/
|
||||
@Test
|
||||
public void shouldRoundTripDefinitionSnapshot() throws Exception {
|
||||
ChainDefinition definition = new ChainDefinition();
|
||||
StartNode startNode = node(new StartNode(), "start");
|
||||
EndNode endNode = node(new EndNode(), "end");
|
||||
Edge edge = edge("start-to-end", "start", "end");
|
||||
edge.setCondition((chain, currentEdge, result) -> true);
|
||||
definition.addNode(startNode);
|
||||
definition.addNode(endNode);
|
||||
definition.addEdge(edge);
|
||||
|
||||
byte[] bytes;
|
||||
try (ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
ObjectOutputStream objectOutput = new ObjectOutputStream(output)) {
|
||||
objectOutput.writeObject(definition);
|
||||
bytes = output.toByteArray();
|
||||
}
|
||||
|
||||
ChainDefinition restored;
|
||||
try (ObjectInputStream objectInput =
|
||||
new ObjectInputStream(new ByteArrayInputStream(bytes))) {
|
||||
restored = (ChainDefinition) objectInput.readObject();
|
||||
}
|
||||
|
||||
Assert.assertEquals(2, restored.getNodes().size());
|
||||
Assert.assertEquals(1, restored.getEdges().size());
|
||||
Assert.assertEquals("end", restored.getOutwardEdge("start").get(0).getTarget());
|
||||
Assert.assertNotNull(restored.getOutwardEdge("start").get(0).getCondition());
|
||||
}
|
||||
|
||||
/**
|
||||
* 为测试节点设置 ID。
|
||||
*
|
||||
* @param node 节点
|
||||
* @param id 节点 ID
|
||||
* @param <T> 节点类型
|
||||
* @return 设置完成的节点
|
||||
*/
|
||||
private <T extends com.easyagents.flow.core.chain.Node> T node(T node, String id) {
|
||||
node.setId(id);
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建测试边。
|
||||
*
|
||||
* @param id 边 ID
|
||||
* @param source 来源节点 ID
|
||||
* @param target 目标节点 ID
|
||||
* @return 测试边
|
||||
*/
|
||||
private Edge edge(String id, String source, String target) {
|
||||
Edge edge = new Edge();
|
||||
edge.setId(id);
|
||||
edge.setSource(source);
|
||||
edge.setTarget(target);
|
||||
return edge;
|
||||
}
|
||||
}
|
||||
@@ -15,20 +15,31 @@
|
||||
*/
|
||||
package com.easyagents.flow.core.test;
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
import com.easyagents.flow.core.chain.ChainStatus;
|
||||
import com.easyagents.flow.core.chain.Edge;
|
||||
import com.easyagents.flow.core.chain.ChainState;
|
||||
import com.easyagents.flow.core.chain.repository.ChainDefinitionSnapshotRepository;
|
||||
import com.easyagents.flow.core.chain.repository.ChainStateField;
|
||||
import com.easyagents.flow.core.chain.repository.ChainStateRepository;
|
||||
import com.easyagents.flow.core.chain.repository.InMemoryChainDefinitionSnapshotRepository;
|
||||
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.EndNode;
|
||||
import com.easyagents.flow.core.node.BaseNode;
|
||||
import com.easyagents.flow.core.node.StartNode;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
@@ -37,6 +48,7 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* {@link ChainExecutor} 并发同步执行测试。
|
||||
@@ -85,6 +97,298 @@ public class ChainExecutorConcurrencyTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证一个工作流实例的多个节点触发会复用启动时的定义快照。
|
||||
*/
|
||||
@Test
|
||||
public void shouldReuseDefinitionSnapshotAcrossNodeTriggers() {
|
||||
ScheduledExecutorService schedulerPool = Executors.newSingleThreadScheduledExecutor();
|
||||
ExecutorService workerPool = Executors.newFixedThreadPool(2);
|
||||
TriggerScheduler triggerScheduler = new TriggerScheduler(
|
||||
new InMemoryTriggerStore(), schedulerPool, workerPool, 1000L);
|
||||
ChainDefinition definition = createDefinition();
|
||||
AtomicInteger definitionLoadCount = new AtomicInteger();
|
||||
ChainExecutor chainExecutor = new ChainExecutor(
|
||||
id -> {
|
||||
definitionLoadCount.incrementAndGet();
|
||||
return definition;
|
||||
},
|
||||
new InMemoryChainStateRepository(),
|
||||
new InMemoryNodeStateRepository(),
|
||||
triggerScheduler);
|
||||
|
||||
try {
|
||||
Map<String, Object> result = chainExecutor.execute(
|
||||
definition.getId(), Collections.emptyMap(), 10, TimeUnit.SECONDS);
|
||||
|
||||
Assert.assertNotNull(result);
|
||||
Assert.assertEquals(1, definitionLoadCount.get());
|
||||
} finally {
|
||||
triggerScheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证取消期间已经完成的节点 I/O 不会继续推进下游节点。
|
||||
*
|
||||
* @throws Exception 等待异步节点进入或退出失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldNotAdvanceAfterCancellationDuringNodeIo() throws Exception {
|
||||
ScheduledExecutorService schedulerPool = Executors.newScheduledThreadPool(2);
|
||||
ExecutorService workerPool = Executors.newFixedThreadPool(2);
|
||||
TriggerScheduler triggerScheduler = new TriggerScheduler(
|
||||
new InMemoryTriggerStore(), schedulerPool, workerPool, 1000L);
|
||||
InMemoryChainStateRepository chainStateRepository = new InMemoryChainStateRepository();
|
||||
CountDownLatch ioStarted = new CountDownLatch(1);
|
||||
CountDownLatch allowIoCompletion = new CountDownLatch(1);
|
||||
AtomicInteger downstreamExecutions = new AtomicInteger();
|
||||
ChainDefinition definition = createCancellationDefinition(
|
||||
ioStarted, allowIoCompletion, downstreamExecutions);
|
||||
ChainExecutor chainExecutor = new ChainExecutor(
|
||||
id -> definition,
|
||||
chainStateRepository,
|
||||
new InMemoryNodeStateRepository(),
|
||||
triggerScheduler);
|
||||
|
||||
try {
|
||||
String instanceId = chainExecutor.executeAsync(
|
||||
definition.getId(), Collections.emptyMap());
|
||||
Assert.assertTrue(ioStarted.await(2, TimeUnit.SECONDS));
|
||||
Assert.assertTrue(chainExecutor.cancel(instanceId, "test cancellation"));
|
||||
allowIoCompletion.countDown();
|
||||
|
||||
Thread.sleep(200L);
|
||||
Assert.assertEquals(0, downstreamExecutions.get());
|
||||
Assert.assertEquals(
|
||||
ChainStatus.CANCELLED,
|
||||
chainStateRepository.load(instanceId).getStatus());
|
||||
} finally {
|
||||
allowIoCompletion.countDown();
|
||||
triggerScheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证同一节点执行期间重复读取状态会复用执行快照。
|
||||
*/
|
||||
@Test
|
||||
public void shouldKeepPublicStateReadsFreshDuringNodeExecution() {
|
||||
ScheduledExecutorService schedulerPool = Executors.newSingleThreadScheduledExecutor();
|
||||
ExecutorService workerPool = Executors.newSingleThreadExecutor();
|
||||
TriggerScheduler triggerScheduler = new TriggerScheduler(
|
||||
new InMemoryTriggerStore(), schedulerPool, workerPool, 1000L);
|
||||
CountingChainStateRepository stateRepository = new CountingChainStateRepository();
|
||||
AtomicInteger nodeStateLoads = new AtomicInteger(-1);
|
||||
ChainDefinition definition = createStateReadDefinition(
|
||||
stateRepository.loadCount,
|
||||
nodeStateLoads,
|
||||
false);
|
||||
ChainExecutor chainExecutor = new ChainExecutor(
|
||||
id -> definition,
|
||||
stateRepository,
|
||||
new InMemoryNodeStateRepository(),
|
||||
triggerScheduler);
|
||||
|
||||
try {
|
||||
Map<String, Object> result = chainExecutor.execute(
|
||||
definition.getId(), Collections.emptyMap(), 10, TimeUnit.SECONDS);
|
||||
|
||||
Assert.assertNotNull(result);
|
||||
Assert.assertEquals(10, nodeStateLoads.get());
|
||||
} finally {
|
||||
triggerScheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证节点内部执行视图重复读取不会访问状态仓储。
|
||||
*/
|
||||
@Test
|
||||
public void shouldReusePointInTimeStateDuringNodeExecution() {
|
||||
ScheduledExecutorService schedulerPool =
|
||||
Executors.newSingleThreadScheduledExecutor();
|
||||
ExecutorService workerPool =
|
||||
Executors.newSingleThreadExecutor();
|
||||
TriggerScheduler triggerScheduler =
|
||||
new TriggerScheduler(
|
||||
new InMemoryTriggerStore(),
|
||||
schedulerPool,
|
||||
workerPool,
|
||||
1000L);
|
||||
CountingChainStateRepository stateRepository =
|
||||
new CountingChainStateRepository();
|
||||
AtomicInteger nodeStateLoads =
|
||||
new AtomicInteger(-1);
|
||||
ChainDefinition definition =
|
||||
createStateReadDefinition(
|
||||
stateRepository.loadCount,
|
||||
nodeStateLoads,
|
||||
true);
|
||||
ChainExecutor chainExecutor =
|
||||
new ChainExecutor(
|
||||
id -> definition,
|
||||
stateRepository,
|
||||
new InMemoryNodeStateRepository(),
|
||||
triggerScheduler);
|
||||
|
||||
try {
|
||||
Map<String, Object> result =
|
||||
chainExecutor.execute(
|
||||
definition.getId(),
|
||||
Collections.emptyMap(),
|
||||
10,
|
||||
TimeUnit.SECONDS);
|
||||
|
||||
Assert.assertNotNull(result);
|
||||
Assert.assertEquals(
|
||||
0, nodeStateLoads.get());
|
||||
} finally {
|
||||
triggerScheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证升级前 parent-linked 状态首次解析顶级审计 ID 后会持久复用。
|
||||
*/
|
||||
@Test
|
||||
public void shouldBackfillLegacyAuditInstanceIdOnce() {
|
||||
CountingChainStateRepository repository =
|
||||
new CountingChainStateRepository();
|
||||
ChainState root =
|
||||
repository.create("audit-root");
|
||||
ChainState child =
|
||||
repository.create("audit-child");
|
||||
child.setParentInstanceId(
|
||||
root.getInstanceId());
|
||||
child.setAuditInstanceId(null);
|
||||
ChainDefinition definition =
|
||||
new ChainDefinition();
|
||||
definition.setId("audit-definition");
|
||||
Chain chain =
|
||||
new Chain(
|
||||
definition,
|
||||
child.getInstanceId());
|
||||
chain.setChainStateRepository(
|
||||
repository);
|
||||
|
||||
Assert.assertEquals(
|
||||
root.getInstanceId(),
|
||||
chain.getAuditInstanceId());
|
||||
int loadsAfterBackfill =
|
||||
repository.loadCount.get();
|
||||
Assert.assertEquals(
|
||||
root.getInstanceId(),
|
||||
chain.getAuditInstanceId());
|
||||
|
||||
Assert.assertEquals(
|
||||
loadsAfterBackfill + 1,
|
||||
repository.loadCount.get());
|
||||
Assert.assertEquals(
|
||||
root.getInstanceId(),
|
||||
repository.load(
|
||||
child.getInstanceId())
|
||||
.getAuditInstanceId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证实例状态初始化失败时会清理进程缓存和持久定义快照。
|
||||
*/
|
||||
@Test
|
||||
public void shouldCleanupDefinitionSnapshotWhenInitializationFails() throws Exception {
|
||||
ScheduledExecutorService schedulerPool = Executors.newSingleThreadScheduledExecutor();
|
||||
ExecutorService workerPool = Executors.newSingleThreadExecutor();
|
||||
TriggerScheduler triggerScheduler = new TriggerScheduler(
|
||||
new InMemoryTriggerStore(), schedulerPool, workerPool, 1000L);
|
||||
TrackingSnapshotRepository snapshotRepository = new TrackingSnapshotRepository();
|
||||
ChainStateRepository failingStateRepository = new ChainStateRepository() {
|
||||
@Override
|
||||
public ChainState load(String instanceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChainState create(String instanceId) {
|
||||
throw new IllegalStateException("initialization failed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryUpdate(
|
||||
ChainState newState, EnumSet<ChainStateField> fields) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
ChainExecutor executor = new ChainExecutor(
|
||||
id -> createDefinition(),
|
||||
failingStateRepository,
|
||||
new InMemoryNodeStateRepository(),
|
||||
null,
|
||||
snapshotRepository,
|
||||
triggerScheduler,
|
||||
null);
|
||||
|
||||
try {
|
||||
executor.executeAsync("concurrent-sync-test", Collections.emptyMap());
|
||||
Assert.fail("initialization failure must be propagated");
|
||||
} catch (IllegalStateException expected) {
|
||||
Assert.assertEquals("initialization failed", expected.getMessage());
|
||||
} finally {
|
||||
triggerScheduler.shutdown();
|
||||
}
|
||||
|
||||
Assert.assertTrue(snapshotRepository.snapshots.isEmpty());
|
||||
Assert.assertTrue(activeDefinitions(executor).isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证进程内定义热点缓存有固定上限,持久快照仍保留恢复能力。
|
||||
*/
|
||||
@Test
|
||||
public void shouldBoundActiveDefinitionCache() throws Exception {
|
||||
ScheduledExecutorService schedulerPool = Executors.newSingleThreadScheduledExecutor();
|
||||
ExecutorService workerPool = Executors.newSingleThreadExecutor();
|
||||
TriggerScheduler triggerScheduler = new TriggerScheduler(
|
||||
new InMemoryTriggerStore(), schedulerPool, workerPool, 1000L);
|
||||
InMemoryChainDefinitionSnapshotRepository snapshotRepository =
|
||||
new InMemoryChainDefinitionSnapshotRepository();
|
||||
ChainExecutor executor = new ChainExecutor(
|
||||
id -> createDefinition(),
|
||||
new InMemoryChainStateRepository(),
|
||||
new InMemoryNodeStateRepository(),
|
||||
null,
|
||||
snapshotRepository,
|
||||
triggerScheduler,
|
||||
null);
|
||||
java.lang.reflect.Method createChain = ChainExecutor.class.getDeclaredMethod(
|
||||
"createChain", ChainDefinition.class);
|
||||
createChain.setAccessible(true);
|
||||
|
||||
try {
|
||||
for (int index = 0; index < 1100; index++) {
|
||||
createChain.invoke(executor, createDefinition());
|
||||
}
|
||||
} finally {
|
||||
triggerScheduler.shutdown();
|
||||
}
|
||||
|
||||
Assert.assertEquals(1024, activeDefinitions(executor).size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取执行器内部定义缓存,供资源上限回归验证。
|
||||
*
|
||||
* @param executor 工作流执行器
|
||||
* @return 当前定义缓存
|
||||
* @throws Exception 反射访问失败时抛出
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, ChainDefinition> activeDefinitions(
|
||||
ChainExecutor executor) throws Exception {
|
||||
Field field = ChainExecutor.class.getDeclaredField("activeDefinitions");
|
||||
field.setAccessible(true);
|
||||
return (Map<String, ChainDefinition>) field.get(executor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建仅包含开始和结束节点的测试工作流。
|
||||
*
|
||||
@@ -109,4 +413,155 @@ public class ChainExecutorConcurrencyTest {
|
||||
definition.addEdge(edge);
|
||||
return definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用于取消传播验证的工作流。
|
||||
*
|
||||
* @param ioStarted I/O 节点进入信号
|
||||
* @param allowIoCompletion I/O 节点退出信号
|
||||
* @param downstreamExecutions 下游执行计数
|
||||
* @return 测试工作流定义
|
||||
*/
|
||||
private ChainDefinition createCancellationDefinition(CountDownLatch ioStarted,
|
||||
CountDownLatch allowIoCompletion,
|
||||
AtomicInteger downstreamExecutions) {
|
||||
ChainDefinition definition = new ChainDefinition();
|
||||
definition.setId("cancellation-test");
|
||||
|
||||
StartNode start = new StartNode();
|
||||
start.setId("start");
|
||||
BaseNode blocking = new BaseNode() {
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
ioStarted.countDown();
|
||||
try {
|
||||
if (!allowIoCompletion.await(2, TimeUnit.SECONDS)) {
|
||||
throw new IllegalStateException("test I/O release timed out");
|
||||
}
|
||||
} catch (InterruptedException error) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("test I/O interrupted", error);
|
||||
}
|
||||
return Collections.singletonMap("value", "completed");
|
||||
}
|
||||
};
|
||||
blocking.setId("blocking");
|
||||
BaseNode downstream = new BaseNode() {
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
downstreamExecutions.incrementAndGet();
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
};
|
||||
downstream.setId("downstream");
|
||||
EndNode end = new EndNode();
|
||||
end.setId("end");
|
||||
|
||||
definition.addNode(start);
|
||||
definition.addNode(blocking);
|
||||
definition.addNode(downstream);
|
||||
definition.addNode(end);
|
||||
definition.addEdge(edge("start-blocking", "start", "blocking"));
|
||||
definition.addEdge(edge("blocking-downstream", "blocking", "downstream"));
|
||||
definition.addEdge(edge("downstream-end", "downstream", "end"));
|
||||
return definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建包含重复状态读取节点的测试工作流。
|
||||
*
|
||||
* @param repositoryLoadCount 仓储读取计数
|
||||
* @param nodeStateLoads 节点执行期间新增的读取次数
|
||||
* @param useExecutionView 是否读取节点 point-in-time 执行视图
|
||||
* @return 测试工作流定义
|
||||
*/
|
||||
private ChainDefinition createStateReadDefinition(AtomicInteger repositoryLoadCount,
|
||||
AtomicInteger nodeStateLoads,
|
||||
boolean useExecutionView) {
|
||||
ChainDefinition definition = new ChainDefinition();
|
||||
definition.setId("state-snapshot-test");
|
||||
|
||||
StartNode start = new StartNode();
|
||||
start.setId("start");
|
||||
BaseNode repeatedReader = new BaseNode() {
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
int before = repositoryLoadCount.get();
|
||||
for (int index = 0; index < 10; index++) {
|
||||
Assert.assertNotNull(
|
||||
useExecutionView
|
||||
? chain.getExecutionState()
|
||||
: chain.getState());
|
||||
}
|
||||
nodeStateLoads.set(repositoryLoadCount.get() - before);
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
};
|
||||
repeatedReader.setId("reader");
|
||||
EndNode end = new EndNode();
|
||||
end.setId("end");
|
||||
|
||||
definition.addNode(start);
|
||||
definition.addNode(repeatedReader);
|
||||
definition.addNode(end);
|
||||
definition.addEdge(edge("start-reader", "start", "reader"));
|
||||
definition.addEdge(edge("reader-end", "reader", "end"));
|
||||
return definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建测试边。
|
||||
*
|
||||
* @param id 边 ID
|
||||
* @param source 源节点 ID
|
||||
* @param target 目标节点 ID
|
||||
* @return 测试边
|
||||
*/
|
||||
private 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 TrackingSnapshotRepository
|
||||
implements ChainDefinitionSnapshotRepository {
|
||||
|
||||
private final Map<String, ChainDefinition> snapshots =
|
||||
new LinkedHashMap<>();
|
||||
|
||||
@Override
|
||||
public void save(String instanceId, ChainDefinition definition) {
|
||||
snapshots.put(instanceId, definition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChainDefinition load(String instanceId) {
|
||||
return snapshots.get(instanceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(String instanceId) {
|
||||
snapshots.remove(instanceId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录状态读取次数的测试仓储。
|
||||
*/
|
||||
private static final class CountingChainStateRepository
|
||||
extends InMemoryChainStateRepository {
|
||||
|
||||
private final AtomicInteger loadCount = new AtomicInteger();
|
||||
|
||||
@Override
|
||||
public ChainState load(String instanceId) {
|
||||
loadCount.incrementAndGet();
|
||||
return super.load(instanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
package com.easyagents.flow.core.test;
|
||||
|
||||
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.EventManager;
|
||||
import com.easyagents.flow.core.chain.NodeState;
|
||||
import com.easyagents.flow.core.chain.NodeStatus;
|
||||
import com.easyagents.flow.core.chain.repository.InMemoryChainStateRepository;
|
||||
import com.easyagents.flow.core.chain.repository.InMemoryNodeStateRepository;
|
||||
import com.easyagents.flow.core.node.StartNode;
|
||||
import com.easyagents.flow.core.chain.runtime.InMemoryTriggerStore;
|
||||
import com.easyagents.flow.core.chain.runtime.Trigger;
|
||||
import com.easyagents.flow.core.chain.runtime.TriggerScheduler;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 验证入口触发器先行持久化和崩溃重放协议。
|
||||
*/
|
||||
public class ChainRecoverableStartTest {
|
||||
|
||||
/**
|
||||
* 验证 RUNNING 状态提交前已经存在入口意图,且 RUNNING 节点允许重放。
|
||||
*/
|
||||
@Test
|
||||
public void shouldPersistIntentBeforeRunningAndReplayInterruptedStart() {
|
||||
InMemoryChainStateRepository stateRepository =
|
||||
new InMemoryChainStateRepository();
|
||||
ObservingTriggerStore triggerStore =
|
||||
new ObservingTriggerStore(stateRepository, "recoverable-1");
|
||||
ScheduledExecutorService schedulerExecutor =
|
||||
Executors.newSingleThreadScheduledExecutor();
|
||||
java.util.concurrent.ExecutorService worker =
|
||||
Executors.newSingleThreadExecutor();
|
||||
TriggerScheduler scheduler = new TriggerScheduler(
|
||||
triggerStore, schedulerExecutor, worker, 60_000L);
|
||||
try {
|
||||
Chain chain = chain(
|
||||
stateRepository, triggerStore, scheduler);
|
||||
chain.start(Collections.singletonMap("value", "v"));
|
||||
|
||||
Assert.assertEquals(
|
||||
ChainStatus.READY,
|
||||
triggerStore.statusObservedAtFirstSave);
|
||||
ChainState running = stateRepository.load("recoverable-1");
|
||||
Assert.assertEquals(ChainStatus.RUNNING, running.getStatus());
|
||||
Trigger pending = triggerStore.find(
|
||||
triggerStore.lastTriggerId);
|
||||
Assert.assertNotNull(pending);
|
||||
|
||||
NodeState startState = chain.getNodeState("start");
|
||||
startState.setStatus(NodeStatus.RUNNING);
|
||||
startState.getExecuteCount().incrementAndGet();
|
||||
chain.start(Collections.emptyMap());
|
||||
|
||||
Assert.assertNotNull(
|
||||
"interrupted RUNNING start must remain replayable",
|
||||
triggerStore.find(triggerStore.lastTriggerId));
|
||||
} finally {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证入口意图保存后、RUNNING 提交前崩溃时,真实调度消费可恢复变量并继续执行。
|
||||
*
|
||||
* @throws Exception 等待异步调度被中断
|
||||
*/
|
||||
@Test
|
||||
public void shouldRecoverReadyStartThroughRealScheduler()
|
||||
throws Exception {
|
||||
InMemoryChainStateRepository stateRepository =
|
||||
new InMemoryChainStateRepository();
|
||||
FailAfterSaveTriggerStore triggerStore =
|
||||
new FailAfterSaveTriggerStore();
|
||||
ScheduledExecutorService schedulerExecutor =
|
||||
Executors.newSingleThreadScheduledExecutor();
|
||||
java.util.concurrent.ExecutorService worker =
|
||||
Executors.newSingleThreadExecutor();
|
||||
TriggerScheduler scheduler = new TriggerScheduler(
|
||||
triggerStore, schedulerExecutor, worker, 1_000L);
|
||||
try {
|
||||
Chain chain = chain(
|
||||
stateRepository, triggerStore, scheduler,
|
||||
"recoverable-crash");
|
||||
StartNode startNode = (StartNode) chain.getDefinition()
|
||||
.getNodeById("start");
|
||||
scheduler.registerConsumer(
|
||||
(trigger, ignored) ->
|
||||
chain.executeNode(startNode, trigger));
|
||||
|
||||
try {
|
||||
chain.start(Collections.singletonMap(
|
||||
"value", "persisted-input"));
|
||||
Assert.fail("simulated crash must interrupt start");
|
||||
} catch (SimulatedCrashException expected) {
|
||||
// 触发器已经保存,异常模拟进程在 RUNNING 提交前退出。
|
||||
}
|
||||
Assert.assertEquals(
|
||||
ChainStatus.READY,
|
||||
stateRepository.load(
|
||||
"recoverable-crash").getStatus());
|
||||
|
||||
long deadline = System.nanoTime()
|
||||
+ TimeUnit.SECONDS.toNanos(5L);
|
||||
while (System.nanoTime() < deadline) {
|
||||
ChainState recovered = stateRepository.load(
|
||||
"recoverable-crash");
|
||||
NodeState nodeState = chain.getNodeState("start");
|
||||
if (recovered.getStatus() != ChainStatus.READY
|
||||
&& nodeState.getStatus()
|
||||
== NodeStatus.SUCCEEDED) {
|
||||
break;
|
||||
}
|
||||
Thread.sleep(25L);
|
||||
}
|
||||
|
||||
ChainState recovered = stateRepository.load(
|
||||
"recoverable-crash");
|
||||
Assert.assertNotEquals(
|
||||
ChainStatus.READY, recovered.getStatus());
|
||||
Assert.assertEquals(
|
||||
"persisted-input",
|
||||
recovered.getMemory().get("value"));
|
||||
Assert.assertEquals(
|
||||
NodeStatus.SUCCEEDED,
|
||||
chain.getNodeState("start").getStatus());
|
||||
Assert.assertNull(triggerStore.find(
|
||||
triggerStore.lastTriggerId));
|
||||
} finally {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建仅含入口节点的测试链。
|
||||
*
|
||||
* @param stateRepository 状态仓储
|
||||
* @param triggerStore 触发器仓储
|
||||
* @param scheduler 调度器
|
||||
* @return 已配置链
|
||||
*/
|
||||
private Chain chain(
|
||||
InMemoryChainStateRepository stateRepository,
|
||||
InMemoryTriggerStore triggerStore,
|
||||
TriggerScheduler scheduler) {
|
||||
ChainDefinition definition = new ChainDefinition();
|
||||
definition.setId("recoverable-definition");
|
||||
StartNode start = new StartNode();
|
||||
start.setId("start");
|
||||
definition.addNode(start);
|
||||
definition.setEdges(Collections.emptyList());
|
||||
|
||||
Chain chain = new Chain(definition, "recoverable-1");
|
||||
chain.setChainStateRepository(stateRepository);
|
||||
chain.setNodeStateRepository(
|
||||
new InMemoryNodeStateRepository());
|
||||
chain.setTriggerScheduler(scheduler);
|
||||
chain.setEventManager(new EventManager());
|
||||
return chain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建指定实例 ID 的仅入口测试链。
|
||||
*
|
||||
* @param stateRepository 状态仓储
|
||||
* @param triggerStore 触发器仓储
|
||||
* @param scheduler 调度器
|
||||
* @param instanceId 实例 ID
|
||||
* @return 已配置链
|
||||
*/
|
||||
private Chain chain(
|
||||
InMemoryChainStateRepository stateRepository,
|
||||
InMemoryTriggerStore triggerStore,
|
||||
TriggerScheduler scheduler,
|
||||
String instanceId) {
|
||||
ChainDefinition definition = new ChainDefinition();
|
||||
definition.setId("recoverable-definition");
|
||||
StartNode start = new StartNode();
|
||||
start.setId("start");
|
||||
definition.addNode(start);
|
||||
definition.setEdges(Collections.emptyList());
|
||||
|
||||
Chain chain = new Chain(definition, instanceId);
|
||||
chain.setChainStateRepository(stateRepository);
|
||||
chain.setNodeStateRepository(
|
||||
new InMemoryNodeStateRepository());
|
||||
chain.setTriggerScheduler(scheduler);
|
||||
chain.setEventManager(new EventManager());
|
||||
return chain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录首次触发器保存时的实例状态。
|
||||
*/
|
||||
private static final class ObservingTriggerStore
|
||||
extends InMemoryTriggerStore {
|
||||
|
||||
private final InMemoryChainStateRepository stateRepository;
|
||||
private final String instanceId;
|
||||
private ChainStatus statusObservedAtFirstSave;
|
||||
private String lastTriggerId;
|
||||
|
||||
/**
|
||||
* 创建观察仓储。
|
||||
*
|
||||
* @param stateRepository 状态仓储
|
||||
* @param instanceId 实例 ID
|
||||
*/
|
||||
private ObservingTriggerStore(
|
||||
InMemoryChainStateRepository stateRepository,
|
||||
String instanceId) {
|
||||
this.stateRepository = stateRepository;
|
||||
this.instanceId = instanceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Trigger save(Trigger trigger) {
|
||||
observe(trigger);
|
||||
return super.save(trigger);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean saveIfAbsent(
|
||||
Trigger trigger) {
|
||||
observe(trigger);
|
||||
return super.saveIfAbsent(trigger);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录首次稳定入口保存时的状态。
|
||||
*
|
||||
* @param trigger 待保存触发器
|
||||
*/
|
||||
private void observe(Trigger trigger) {
|
||||
if (statusObservedAtFirstSave == null) {
|
||||
ChainState state = stateRepository.load(instanceId);
|
||||
statusObservedAtFirstSave =
|
||||
state == null ? null : state.getStatus();
|
||||
}
|
||||
lastTriggerId = trigger.getId();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 首次保存成功后抛错,用于模拟状态提交前进程退出。
|
||||
*/
|
||||
private static final class FailAfterSaveTriggerStore
|
||||
extends InMemoryTriggerStore {
|
||||
|
||||
private boolean fail = true;
|
||||
private String lastTriggerId;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Trigger save(Trigger trigger) {
|
||||
Trigger saved = super.save(trigger);
|
||||
failAfterFirstSave(trigger);
|
||||
return saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean saveIfAbsent(
|
||||
Trigger trigger) {
|
||||
boolean saved =
|
||||
super.saveIfAbsent(trigger);
|
||||
if (saved) {
|
||||
failAfterFirstSave(trigger);
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* 首次成功保存后抛出模拟崩溃。
|
||||
*
|
||||
* @param trigger 已保存触发器
|
||||
*/
|
||||
private void failAfterFirstSave(
|
||||
Trigger trigger) {
|
||||
lastTriggerId = trigger.getId();
|
||||
if (fail) {
|
||||
fail = false;
|
||||
throw new SimulatedCrashException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试专用崩溃异常。
|
||||
*/
|
||||
private static final class SimulatedCrashException
|
||||
extends RuntimeException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
package com.easyagents.flow.core.test;
|
||||
|
||||
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.Parameter;
|
||||
import com.easyagents.flow.core.chain.RefType;
|
||||
import com.easyagents.flow.core.chain.repository.InMemoryLoopResultRepository;
|
||||
import com.easyagents.flow.core.chain.repository.LoopInputReference;
|
||||
import com.easyagents.flow.core.node.StartNode;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -32,4 +38,229 @@ public class ChainTemplateContextTest {
|
||||
|
||||
Assert.assertEquals("7", result.get("nextValue"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证审计参数快照保留直接大型引用,不在工作流线程读取完整输入。
|
||||
*
|
||||
* @throws Exception 线程上下文反射失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldPreserveDirectReferenceForAuditWithoutLoadingInput()
|
||||
throws Exception {
|
||||
CountingLoopRepository repository =
|
||||
new CountingLoopRepository();
|
||||
Chain chain = new Chain(
|
||||
new ChainDefinition(),
|
||||
"instance-audit");
|
||||
chain.setLoopResultRepository(repository);
|
||||
Field currentChain = Chain.class
|
||||
.getDeclaredField(
|
||||
"EXECUTION_THREAD_LOCAL");
|
||||
currentChain.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
ThreadLocal<Chain> context =
|
||||
(ThreadLocal<Chain>) currentChain.get(null);
|
||||
LoopInputReference reference =
|
||||
new LoopInputReference(
|
||||
"instance-audit:dataset",
|
||||
100_000);
|
||||
ChainState state = new ChainState();
|
||||
state.getMemory().put(
|
||||
"dataset.data", reference);
|
||||
Parameter parameter = new Parameter();
|
||||
parameter.setName("items");
|
||||
parameter.setRefType(RefType.REF);
|
||||
parameter.setRef("dataset.data");
|
||||
StartNode node = new StartNode();
|
||||
node.setParameters(List.of(parameter));
|
||||
|
||||
context.set(chain);
|
||||
try {
|
||||
Map<String, Object> result =
|
||||
state.resolveParametersPreservingReferences(
|
||||
node);
|
||||
|
||||
Assert.assertSame(
|
||||
reference, result.get("items"));
|
||||
Assert.assertEquals(
|
||||
0, repository.loadInputCalls);
|
||||
} finally {
|
||||
context.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证无关固定参数不会触发 memory 中大型引用的物化。
|
||||
*
|
||||
* @throws Exception 线程上下文反射失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldNotLoadUnrelatedReferenceForFixedAuditParameter()
|
||||
throws Exception {
|
||||
CountingLoopRepository repository =
|
||||
new CountingLoopRepository();
|
||||
Chain chain = new Chain(
|
||||
new ChainDefinition(),
|
||||
"instance-fixed-audit");
|
||||
chain.setLoopResultRepository(repository);
|
||||
Field currentChain = Chain.class
|
||||
.getDeclaredField(
|
||||
"EXECUTION_THREAD_LOCAL");
|
||||
currentChain.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
ThreadLocal<Chain> context =
|
||||
(ThreadLocal<Chain>) currentChain.get(null);
|
||||
ChainState state = new ChainState();
|
||||
state.getMemory().put(
|
||||
"dataset.data",
|
||||
new LoopInputReference(
|
||||
"instance-fixed-audit:dataset",
|
||||
100_000));
|
||||
state.getMemory().put(
|
||||
"small.value", "ok");
|
||||
Parameter parameter = new Parameter();
|
||||
parameter.setName("description");
|
||||
parameter.setRefType(RefType.FIXED);
|
||||
parameter.setValue("{{small.value}}");
|
||||
StartNode node = new StartNode();
|
||||
node.setParameters(List.of(parameter));
|
||||
|
||||
context.set(chain);
|
||||
try {
|
||||
Map<String, Object> result =
|
||||
state.resolveParametersPreservingReferences(
|
||||
node);
|
||||
|
||||
Assert.assertEquals(
|
||||
"ok",
|
||||
result.get("description"));
|
||||
Assert.assertEquals(
|
||||
0, repository.loadInputCalls);
|
||||
} finally {
|
||||
context.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证普通节点固定参数同样只还原模板实际读取的大型引用。
|
||||
*
|
||||
* @throws Exception 线程上下文反射失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldNotLoadUnrelatedReferenceForNormalFixedParameter()
|
||||
throws Exception {
|
||||
CountingLoopRepository repository =
|
||||
new CountingLoopRepository();
|
||||
Chain chain = new Chain(
|
||||
new ChainDefinition(),
|
||||
"instance-fixed-normal");
|
||||
chain.setLoopResultRepository(repository);
|
||||
Field currentChain = Chain.class
|
||||
.getDeclaredField(
|
||||
"EXECUTION_THREAD_LOCAL");
|
||||
currentChain.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
ThreadLocal<Chain> context =
|
||||
(ThreadLocal<Chain>) currentChain.get(null);
|
||||
ChainState state = new ChainState();
|
||||
state.getMemory().put(
|
||||
"dataset.data",
|
||||
new LoopInputReference(
|
||||
"instance-fixed-normal:dataset",
|
||||
100_000));
|
||||
state.getMemory().put(
|
||||
"small.value", "ok");
|
||||
Parameter parameter = new Parameter();
|
||||
parameter.setName("description");
|
||||
parameter.setRefType(RefType.FIXED);
|
||||
parameter.setValue("{{small.value}}");
|
||||
StartNode node = new StartNode();
|
||||
node.setParameters(List.of(parameter));
|
||||
|
||||
context.set(chain);
|
||||
try {
|
||||
Map<String, Object> result =
|
||||
state.resolveParameters(node);
|
||||
|
||||
Assert.assertEquals(
|
||||
"ok",
|
||||
result.get("description"));
|
||||
Assert.assertEquals(
|
||||
0, repository.loadInputCalls);
|
||||
} finally {
|
||||
context.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证同一模板重复读取一个大型引用时只执行一次仓储还原。
|
||||
*
|
||||
* @throws Exception 线程上下文反射失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldMemoizeRepeatedReferenceWithinOneTemplate()
|
||||
throws Exception {
|
||||
CountingLoopRepository repository =
|
||||
new CountingLoopRepository();
|
||||
String resultId =
|
||||
"instance-fixed-repeat:dataset";
|
||||
repository.storeInput(
|
||||
resultId, List.of("item"));
|
||||
Chain chain = new Chain(
|
||||
new ChainDefinition(),
|
||||
"instance-fixed-repeat");
|
||||
chain.setLoopResultRepository(repository);
|
||||
Field currentChain = Chain.class
|
||||
.getDeclaredField(
|
||||
"EXECUTION_THREAD_LOCAL");
|
||||
currentChain.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
ThreadLocal<Chain> context =
|
||||
(ThreadLocal<Chain>) currentChain.get(null);
|
||||
ChainState state = new ChainState();
|
||||
state.getMemory().put(
|
||||
"dataset.data",
|
||||
new LoopInputReference(
|
||||
resultId, 1));
|
||||
Parameter parameter = new Parameter();
|
||||
parameter.setName("description");
|
||||
parameter.setRefType(RefType.FIXED);
|
||||
parameter.setValue(
|
||||
"{{dataset.data}}/{{dataset.data}}");
|
||||
StartNode node = new StartNode();
|
||||
node.setParameters(List.of(parameter));
|
||||
|
||||
context.set(chain);
|
||||
try {
|
||||
Map<String, Object> result =
|
||||
state.resolveParameters(node);
|
||||
|
||||
Assert.assertEquals(
|
||||
"[item]/[item]",
|
||||
result.get("description"));
|
||||
Assert.assertEquals(
|
||||
1, repository.loadInputCalls);
|
||||
} finally {
|
||||
context.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录大型输入加载次数的仓储。
|
||||
*/
|
||||
private static final class CountingLoopRepository
|
||||
extends InMemoryLoopResultRepository {
|
||||
|
||||
private int loadInputCalls;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public List<Object> loadInput(
|
||||
LoopInputReference reference) {
|
||||
loadInputCalls++;
|
||||
return super.loadInput(reference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.easyagents.flow.core.test;
|
||||
|
||||
import com.easyagents.flow.core.chain.runtime.ExecutionBudget;
|
||||
import com.easyagents.flow.core.chain.runtime.ExecutionBudgetExceededException;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* 验证工作流执行预算的宽松默认值和边界检查。
|
||||
*/
|
||||
public class ExecutionBudgetTest {
|
||||
|
||||
/**
|
||||
* 验证缺省预算与 XL12 约定一致。
|
||||
*/
|
||||
@Test
|
||||
public void shouldUseGenerousDefaultBudgets() {
|
||||
ExecutionBudget budget = ExecutionBudget.defaults();
|
||||
|
||||
Assert.assertEquals(100_000L, budget.getMaxIterations());
|
||||
// 人工确认和长期挂起不应被墙钟时间误伤,时长预算默认关闭。
|
||||
Assert.assertEquals(0L, budget.getMaxDurationMillis());
|
||||
Assert.assertEquals(1_000_000L, budget.getMaxChildExecutions());
|
||||
Assert.assertEquals(512L * 1024L * 1024L, budget.getMaxAccumulatedBytes());
|
||||
Assert.assertEquals(32, budget.getMaxNestedDepth());
|
||||
// 热状态估算可能误伤既有业务,默认关闭并允许部署方显式配置。
|
||||
Assert.assertEquals(0L, budget.getMaxHotStateBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证达到阈值时仍允许执行,超过阈值后才阻止。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectOnlyAfterBudgetIsExceeded() {
|
||||
ExecutionBudget budget = new ExecutionBudget(3, 1000, 5, 10, 2, 20);
|
||||
|
||||
budget.checkIterations("loop", 3);
|
||||
budget.checkDuration(1000, 2000);
|
||||
budget.checkChildExecutions(5);
|
||||
budget.checkAccumulatedBytes("loop", 10);
|
||||
budget.checkNestedDepth("loop", 2);
|
||||
|
||||
assertExceeded(() -> budget.checkIterations("loop", 4));
|
||||
assertExceeded(() -> budget.checkDuration(1000, 2001));
|
||||
assertExceeded(() -> budget.checkChildExecutions(6));
|
||||
assertExceeded(() -> budget.checkAccumulatedBytes("loop", 11));
|
||||
assertExceeded(() -> budget.checkNestedDepth("loop", 3));
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言动作触发预算超限异常。
|
||||
*
|
||||
* @param action 待执行动作
|
||||
*/
|
||||
private void assertExceeded(Runnable action) {
|
||||
try {
|
||||
action.run();
|
||||
Assert.fail("Expected ExecutionBudgetExceededException");
|
||||
} catch (ExecutionBudgetExceededException expected) {
|
||||
Assert.assertNotNull(expected.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
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.Edge;
|
||||
import com.easyagents.flow.core.chain.Node;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.chain.RefType;
|
||||
import com.easyagents.flow.core.chain.repository.ChainDefinitionRepository;
|
||||
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.node.BaseNode;
|
||||
import com.easyagents.flow.core.node.EndNode;
|
||||
import com.easyagents.flow.core.node.LoopNode;
|
||||
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.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 验证普通节点循环的总次数语义和嵌套计数隔离。
|
||||
*/
|
||||
public class GenericNodeLoopCountTest {
|
||||
|
||||
/**
|
||||
* 验证默认次数为 1,且零值和超过 300 的值均被拒绝。
|
||||
*/
|
||||
@Test
|
||||
public void shouldValidateConfiguredLoopCount() {
|
||||
ProbeNode node = new ProbeNode();
|
||||
|
||||
Assert.assertEquals(Node.MIN_LOOP_COUNT, node.getMaxLoopCount());
|
||||
assertInvalidLoopCount(node, 0);
|
||||
assertInvalidLoopCount(node, Node.MAX_LOOP_COUNT + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 JSON 解析缺省为 1,并拒绝零值、小数和超过上限的配置。
|
||||
*/
|
||||
@Test
|
||||
public void shouldValidateParsedLoopCount() {
|
||||
ChainParser parser = ChainParser.builder()
|
||||
.withDefaultParsers(true)
|
||||
.build();
|
||||
Node defaultNode = parseLoopNodeConfiguration(parser, null);
|
||||
|
||||
Assert.assertEquals(
|
||||
Node.MIN_LOOP_COUNT,
|
||||
defaultNode.getMaxLoopCount());
|
||||
assertInvalidParsedLoopCount(parser, "0");
|
||||
assertInvalidParsedLoopCount(parser, "1.5");
|
||||
assertInvalidParsedLoopCount(
|
||||
parser,
|
||||
String.valueOf(Node.MAX_LOOP_COUNT + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证填写 1 执行 1 次,填写 3 执行 3 次。
|
||||
*/
|
||||
@Test
|
||||
public void shouldExecuteConfiguredTotalCount() {
|
||||
Map<String, Object> once = createExecutor(
|
||||
createGenericDefinition("generic-once", 1))
|
||||
.execute("generic-once", Collections.emptyMap());
|
||||
Map<String, Object> threeTimes = createExecutor(
|
||||
createGenericDefinition("generic-three", 3))
|
||||
.execute("generic-three", Collections.emptyMap());
|
||||
|
||||
Assert.assertEquals(0, once.get("loopIndex"));
|
||||
Assert.assertEquals(2, threeTimes.get("loopIndex"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证显式外层循环每一轮都会让普通内层节点从索引 0 重新开始。
|
||||
*/
|
||||
@Test
|
||||
public void shouldResetInnerGenericLoopForEveryOuterIteration() {
|
||||
ChainExecutor executor = createExecutor(createNestedDefinition());
|
||||
|
||||
Map<String, Object> result = executor.execute(
|
||||
"nested-loop-count",
|
||||
Collections.singletonMap("times", 3));
|
||||
|
||||
Assert.assertEquals(
|
||||
Arrays.asList(1, 1, 1),
|
||||
result.get("loopIndexes"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建普通循环定义。
|
||||
*
|
||||
* @param id 定义 ID
|
||||
* @param loopCount 总执行次数
|
||||
* @return 工作流定义
|
||||
*/
|
||||
private ChainDefinition createGenericDefinition(
|
||||
String id, int loopCount) {
|
||||
ChainDefinition definition = new ChainDefinition();
|
||||
definition.setId(id);
|
||||
|
||||
StartNode start = new StartNode();
|
||||
start.setId("start");
|
||||
|
||||
ProbeNode probe = new ProbeNode();
|
||||
probe.setId("probe");
|
||||
probe.setLoopEnable(true);
|
||||
probe.setLoopIntervalMs(0L);
|
||||
probe.setMaxLoopCount(loopCount);
|
||||
|
||||
EndNode end = new EndNode();
|
||||
end.setId("end");
|
||||
Parameter output = new Parameter();
|
||||
output.setName("loopIndex");
|
||||
output.setRef("probe.loopIndex");
|
||||
output.setRefType(RefType.REF);
|
||||
end.setOutputDefs(Collections.singletonList(output));
|
||||
|
||||
definition.addNode(start);
|
||||
definition.addNode(probe);
|
||||
definition.addNode(end);
|
||||
definition.addEdge(edge("e1", "start", "probe"));
|
||||
definition.addEdge(edge("e2", "probe", "end"));
|
||||
return definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建显式外层循环嵌套普通内层循环的定义。
|
||||
*
|
||||
* @return 工作流定义
|
||||
*/
|
||||
private ChainDefinition createNestedDefinition() {
|
||||
ChainDefinition definition = new ChainDefinition();
|
||||
definition.setId("nested-loop-count");
|
||||
|
||||
StartNode start = new StartNode();
|
||||
start.setId("start");
|
||||
start.setParameters(Collections.singletonList(inputParameter("times")));
|
||||
|
||||
LoopNode loop = new LoopNode();
|
||||
loop.setId("loop");
|
||||
Parameter loopVar = new Parameter();
|
||||
loopVar.setName("times");
|
||||
loopVar.setRef("times");
|
||||
loopVar.setRefType(RefType.REF);
|
||||
loop.setLoopVar(loopVar);
|
||||
|
||||
ProbeNode probe = new ProbeNode();
|
||||
probe.setId("probe");
|
||||
probe.setParentId("loop");
|
||||
probe.setLoopEnable(true);
|
||||
probe.setLoopIntervalMs(0L);
|
||||
probe.setMaxLoopCount(2);
|
||||
|
||||
Parameter loopOutput = new Parameter();
|
||||
loopOutput.setName("loopIndex");
|
||||
loopOutput.setRef("probe.loopIndex");
|
||||
loopOutput.setRefType(RefType.REF);
|
||||
loop.setOutputDefs(Collections.singletonList(loopOutput));
|
||||
|
||||
EndNode end = new EndNode();
|
||||
end.setId("end");
|
||||
Parameter result = new Parameter();
|
||||
result.setName("loopIndexes");
|
||||
result.setRef("loop.loopIndex");
|
||||
result.setRefType(RefType.REF);
|
||||
end.setOutputDefs(Collections.singletonList(result));
|
||||
|
||||
definition.addNode(start);
|
||||
definition.addNode(loop);
|
||||
definition.addNode(probe);
|
||||
definition.addNode(end);
|
||||
definition.addEdge(edge("e1", "start", "loop"));
|
||||
definition.addEdge(edge("e2", "loop", "probe"));
|
||||
definition.addEdge(edge("e3", "loop", "end"));
|
||||
return definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建执行器。
|
||||
*
|
||||
* @param definition 工作流定义
|
||||
* @return 测试执行器
|
||||
*/
|
||||
private ChainExecutor createExecutor(ChainDefinition definition) {
|
||||
return new ChainExecutor(
|
||||
new FixedDefinitionRepository(definition),
|
||||
new InMemoryChainStateRepository(),
|
||||
new InMemoryNodeStateRepository());
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言设置非法循环次数时抛出参数异常。
|
||||
*
|
||||
* @param node 测试节点
|
||||
* @param loopCount 非法次数
|
||||
*/
|
||||
private void assertInvalidLoopCount(Node node, int loopCount) {
|
||||
try {
|
||||
node.setMaxLoopCount(loopCount);
|
||||
Assert.fail("Expected IllegalArgumentException");
|
||||
} catch (IllegalArgumentException expected) {
|
||||
Assert.assertTrue(expected.getMessage().contains("maxLoopCount"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言解析非法循环次数时失败。
|
||||
*
|
||||
* @param parser 工作流解析器
|
||||
* @param loopCount 原始次数 JSON 值
|
||||
*/
|
||||
private void assertInvalidParsedLoopCount(
|
||||
ChainParser parser, String loopCount) {
|
||||
try {
|
||||
parseLoopNodeConfiguration(parser, loopCount);
|
||||
Assert.fail("Expected IllegalArgumentException");
|
||||
} catch (IllegalArgumentException expected) {
|
||||
Assert.assertTrue(expected.getMessage().contains("maxLoopCount"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析一个启用普通循环的结束节点。
|
||||
*
|
||||
* @param parser 工作流解析器
|
||||
* @param loopCount 循环次数;为空时省略字段
|
||||
* @return 解析后的节点
|
||||
*/
|
||||
private Node parseLoopNodeConfiguration(
|
||||
ChainParser parser, String loopCount) {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("loopEnable", true);
|
||||
if (loopCount != null) {
|
||||
data.put("maxLoopCount", loopCount);
|
||||
}
|
||||
JSONObject nodeJson = new JSONObject();
|
||||
nodeJson.put("id", "end");
|
||||
nodeJson.put("type", "endNode");
|
||||
nodeJson.put("data", data);
|
||||
return new EndNodeParser().parse(
|
||||
nodeJson, new JSONObject(), parser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建输入参数。
|
||||
*
|
||||
* @param name 参数名
|
||||
* @return 输入参数
|
||||
*/
|
||||
private Parameter inputParameter(String name) {
|
||||
Parameter parameter = new Parameter();
|
||||
parameter.setName(name);
|
||||
parameter.setRefType(RefType.INPUT);
|
||||
parameter.setRequired(true);
|
||||
return parameter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建连线。
|
||||
*
|
||||
* @param id 连线 ID
|
||||
* @param source 源节点
|
||||
* @param target 目标节点
|
||||
* @return 连线
|
||||
*/
|
||||
private 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 ProbeNode extends BaseNode {
|
||||
|
||||
/**
|
||||
* 执行探针节点。
|
||||
*
|
||||
* @param chain 当前工作流
|
||||
* @return 当前零基循环索引
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
return Collections.singletonMap(
|
||||
"loopIndex",
|
||||
chain.getNodeState(getId()).getLoopCount());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 固定返回测试定义的仓储。
|
||||
*/
|
||||
private static final class FixedDefinitionRepository
|
||||
implements ChainDefinitionRepository {
|
||||
|
||||
private final ChainDefinition definition;
|
||||
|
||||
/**
|
||||
* 创建定义仓储。
|
||||
*
|
||||
* @param definition 工作流定义
|
||||
*/
|
||||
private FixedDefinitionRepository(ChainDefinition definition) {
|
||||
this.definition = definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public ChainDefinition getChainDefinitionById(String id) {
|
||||
return definition;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.easyagents.flow.core.test;
|
||||
|
||||
import com.easyagents.flow.core.chain.runtime.RetryableTriggerException;
|
||||
import com.easyagents.flow.core.util.IoBulkhead;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* {@link IoBulkhead} 有界并发与可观测性回归测试。
|
||||
*/
|
||||
public class IoBulkheadTest {
|
||||
|
||||
/**
|
||||
* 验证单目标饱和时快速拒绝,同时其他目标仍可使用剩余全局容量。
|
||||
*
|
||||
* @throws Exception 并发测试执行失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldIsolateSaturatedTargetWithoutExhaustingGlobalCapacity() throws Exception {
|
||||
IoBulkhead bulkhead = new IoBulkhead(2, 1, Duration.ofMillis(50), 4);
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
try (IoBulkhead.Permit first = bulkhead.acquire("http:first");
|
||||
IoBulkhead.Permit second = bulkhead.acquire("http:second")) {
|
||||
Future<?> rejected = executor.submit(() -> bulkhead.acquire("http:first"));
|
||||
try {
|
||||
rejected.get();
|
||||
Assert.fail("same target should be rejected");
|
||||
} catch (ExecutionException exception) {
|
||||
Assert.assertTrue(exception.getCause() instanceof RetryableTriggerException);
|
||||
}
|
||||
|
||||
IoBulkhead.Snapshot snapshot = bulkhead.snapshot();
|
||||
Assert.assertEquals(2L, snapshot.acquiredCount());
|
||||
Assert.assertEquals(1L, snapshot.rejectedCount());
|
||||
Assert.assertEquals(2L, snapshot.inFlightCount());
|
||||
Assert.assertEquals(0, snapshot.availableGlobalPermits());
|
||||
Assert.assertEquals(2, snapshot.trackedTargetCount());
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
|
||||
Assert.assertEquals(0L, bulkhead.snapshot().inFlightCount());
|
||||
Assert.assertEquals(2, bulkhead.snapshot().availableGlobalPermits());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 URL 目标键只保留协议无关的主机和显式端口。
|
||||
*/
|
||||
@Test
|
||||
public void shouldResolveStableHttpTarget() {
|
||||
Assert.assertEquals(
|
||||
"http:example.com:8443",
|
||||
IoBulkhead.targetForUrl("https://EXAMPLE.com:8443/path?q=1"));
|
||||
Assert.assertEquals("http:unknown", IoBulkhead.targetForUrl("not a url"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证高基数目标不会突破目标信号量注册上限。
|
||||
*/
|
||||
@Test
|
||||
public void shouldKeepTrackedTargetRegistryBounded() {
|
||||
IoBulkhead bulkhead = new IoBulkhead(4, 2, Duration.ofMillis(50), 2);
|
||||
|
||||
try (IoBulkhead.Permit ignored = bulkhead.acquire("target:first")) {
|
||||
// 仅触发目标注册。
|
||||
}
|
||||
try (IoBulkhead.Permit ignored = bulkhead.acquire("target:second")) {
|
||||
// 仅触发目标注册。
|
||||
}
|
||||
try (IoBulkhead.Permit ignored = bulkhead.acquire("target:overflow")) {
|
||||
// 超额目标统一使用有界的 overflow 信号量。
|
||||
}
|
||||
|
||||
Assert.assertEquals(2, bulkhead.snapshot().trackedTargetCount());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.easyagents.flow.core.test;
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
import com.easyagents.flow.core.chain.repository.InMemoryChainStateRepository;
|
||||
import com.easyagents.flow.core.util.JsConditionUtil;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* GraalVM 条件表达式编译复用与上下文隔离回归测试。
|
||||
*/
|
||||
public class JsConditionUtilTest {
|
||||
|
||||
/**
|
||||
* 验证相同 Source 在不同 Context 中不会残留上一次变量。
|
||||
*/
|
||||
@Test
|
||||
public void shouldIsolateVariablesWhenReusingSource() {
|
||||
Chain chain = chain("js-isolation");
|
||||
|
||||
Assert.assertTrue(JsConditionUtil.eval(
|
||||
"value > 10", chain, Map.of("value", 11)));
|
||||
Assert.assertFalse(JsConditionUtil.eval(
|
||||
"value > 10", chain, Map.of("value", 9)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证共享 Engine 和 Source 可被多个隔离 Context 并发使用。
|
||||
*
|
||||
* @throws Exception 等待并发任务被中断时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldEvaluateSharedSourceConcurrently()
|
||||
throws Exception {
|
||||
Chain chain = chain("js-concurrent");
|
||||
ExecutorService workers =
|
||||
Executors.newFixedThreadPool(4);
|
||||
CountDownLatch completed =
|
||||
new CountDownLatch(20);
|
||||
AtomicInteger failures =
|
||||
new AtomicInteger();
|
||||
try {
|
||||
for (int value = 0; value < 20; value++) {
|
||||
int current = value;
|
||||
workers.submit(() -> {
|
||||
try {
|
||||
boolean result = JsConditionUtil.eval(
|
||||
"value % 2 === 0",
|
||||
chain,
|
||||
Map.of("value", current));
|
||||
if (result != (current % 2 == 0)) {
|
||||
failures.incrementAndGet();
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
failures.incrementAndGet();
|
||||
} finally {
|
||||
completed.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
Assert.assertTrue(completed.await(
|
||||
10L, TimeUnit.SECONDS));
|
||||
Assert.assertEquals(0, failures.get());
|
||||
} finally {
|
||||
workers.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带初始化状态的工作流。
|
||||
*
|
||||
* @param instanceId 实例 ID
|
||||
* @return 测试工作流
|
||||
*/
|
||||
private Chain chain(String instanceId) {
|
||||
Chain chain = new Chain(
|
||||
new ChainDefinition(), instanceId);
|
||||
chain.setChainStateRepository(
|
||||
new InMemoryChainStateRepository());
|
||||
chain.initializeState();
|
||||
return chain;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.easyagents.flow.core.test;
|
||||
|
||||
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.NodeState;
|
||||
import com.easyagents.flow.core.chain.repository.InMemoryChainStateRepository;
|
||||
import com.easyagents.flow.core.chain.repository.InMemoryLoopResultRepository;
|
||||
import com.easyagents.flow.core.node.LoopNode;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectStreamClass;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 校验工作流 Java 序列化状态的滚动升级兼容性。
|
||||
*/
|
||||
public class LegacySerializationCompatibilityTest {
|
||||
|
||||
private static final String LEGACY_LOOP_CONTEXT =
|
||||
"rO0ABXNyADJjb20uZWFzeWFnZW50cy5mbG93LmNvcmUubm9kZS5Mb29wTm9k"
|
||||
+ "ZSRMb29wQ29udGV4dEj5b620EJczAgACSQAMY3VycmVudEluZGV4TAAJc3Vi"
|
||||
+ "UmVzdWx0dAAPTGphdmEvdXRpbC9NYXA7eHAAAAADc3IAF2phdmEudXRpbC5M"
|
||||
+ "aW5rZWRIYXNoTWFwNMBOXBBswPsCAAFaAAthY2Nlc3NPcmRlcnhyABFqYXZh"
|
||||
+ "LnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVz"
|
||||
+ "aG9sZHhwP0AAAAAAAAx3CAAAABAAAAABdAAFdmFsdWVzcgATamF2YS51dGls"
|
||||
+ "LkFycmF5TGlzdHiB0h2Zx2GdAwABSQAEc2l6ZXhwAAAAAncEAAAAAnQAAWF0"
|
||||
+ "AAFieHgA";
|
||||
|
||||
/**
|
||||
* 保护历史类的默认序列化 UID,避免缓存状态在滚动升级时失效。
|
||||
*/
|
||||
@Test
|
||||
public void shouldKeepLegacySerialVersionUids() {
|
||||
Assert.assertEquals(-7958235553581638052L,
|
||||
ObjectStreamClass.lookup(ChainState.class).getSerialVersionUID());
|
||||
Assert.assertEquals(-6727481826462129573L,
|
||||
ObjectStreamClass.lookup(NodeState.class).getSerialVersionUID());
|
||||
Assert.assertEquals(5258356831772776243L,
|
||||
ObjectStreamClass.lookup(LoopNode.LoopContext.class).getSerialVersionUID());
|
||||
}
|
||||
|
||||
/**
|
||||
* 旧 LoopContext 中的内嵌结果应惰性迁移到分块结果仓储。
|
||||
*
|
||||
* @throws Exception 反序列化或反射调用失败
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void shouldMigrateLegacyLoopResultWithoutDataLoss() throws Exception {
|
||||
LoopNode.LoopContext context;
|
||||
byte[] bytes = Base64.getDecoder().decode(LEGACY_LOOP_CONTEXT);
|
||||
try (ObjectInputStream input = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
|
||||
context = (LoopNode.LoopContext) input.readObject();
|
||||
}
|
||||
Assert.assertEquals(3, context.getCurrentIndex());
|
||||
Assert.assertEquals(List.of("a", "b"), context.getSubResult().get("value"));
|
||||
Assert.assertNull(context.getResultId());
|
||||
|
||||
InMemoryLoopResultRepository resultRepository = new InMemoryLoopResultRepository();
|
||||
Chain chain = new Chain(new ChainDefinition(), "legacy-chain");
|
||||
chain.setChainStateRepository(new InMemoryChainStateRepository());
|
||||
chain.setLoopResultRepository(resultRepository);
|
||||
|
||||
LoopNode loopNode = new LoopNode();
|
||||
Method migrate = LoopNode.class.getDeclaredMethod(
|
||||
"migrateLegacyResult", Chain.class, LoopNode.LoopContext.class);
|
||||
migrate.setAccessible(true);
|
||||
migrate.invoke(loopNode, chain, context);
|
||||
|
||||
Assert.assertNotNull(context.getResultId());
|
||||
Assert.assertNull(context.getSubResult());
|
||||
Map<String, Object> migrated =
|
||||
resultRepository.load(context.getResultId(), 2, List.of("value"));
|
||||
Assert.assertEquals(List.of("a", "b"), migrated.get("value"));
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,18 @@ package com.easyagents.flow.core.test;
|
||||
import com.easyagents.flow.core.chain.*;
|
||||
import com.easyagents.flow.core.chain.repository.ChainDefinitionRepository;
|
||||
import com.easyagents.flow.core.chain.repository.InMemoryChainStateRepository;
|
||||
import com.easyagents.flow.core.chain.repository.InMemoryLoopResultRepository;
|
||||
import com.easyagents.flow.core.chain.repository.InMemoryNodeStateRepository;
|
||||
import com.easyagents.flow.core.chain.repository.LoopInputReference;
|
||||
import com.easyagents.flow.core.chain.repository.NodeStateField;
|
||||
import com.easyagents.flow.core.chain.repository.NodeStateRepository;
|
||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||
import com.easyagents.flow.core.chain.runtime.ExecutionBudget;
|
||||
import com.easyagents.flow.core.chain.runtime.ExecutionBudgetExceededException;
|
||||
import com.easyagents.flow.core.chain.runtime.InMemoryTriggerStore;
|
||||
import com.easyagents.flow.core.chain.runtime.TriggerScheduler;
|
||||
import com.easyagents.flow.core.chain.event.NodeEndEvent;
|
||||
import com.easyagents.flow.core.chain.event.NodeStartEvent;
|
||||
import com.easyagents.flow.core.node.BaseNode;
|
||||
import com.easyagents.flow.core.node.EndNode;
|
||||
import com.easyagents.flow.core.node.LoopNode;
|
||||
@@ -12,9 +22,19 @@ import com.easyagents.flow.core.node.StartNode;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.AbstractList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* 验证循环体内可以读取父循环节点上一轮的最新输出。
|
||||
@@ -23,6 +43,287 @@ public class LoopNodeProgressContextTest {
|
||||
|
||||
@Test
|
||||
public void shouldExposeLatestLoopOutputInsideLoopBody() {
|
||||
ChainDefinition definition = createDefinition();
|
||||
ChainExecutor executor = createExecutor(definition);
|
||||
Map<String, Object> variables = new HashMap<>();
|
||||
variables.put("times", 2);
|
||||
|
||||
Map<String, Object> resultMap = executor.execute("loop-progress-test", variables);
|
||||
|
||||
Assert.assertEquals(Arrays.asList("1", "2"), resultMap.get("result"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证循环节点跨子触发器完成时复用同一稳定业务尝试键。
|
||||
*/
|
||||
@Test
|
||||
public void shouldKeepLoopExecutionAttemptKeyStable() {
|
||||
ChainDefinition definition =
|
||||
createDefinition();
|
||||
ChainExecutor executor =
|
||||
createExecutor(definition);
|
||||
java.util.concurrent.atomic.AtomicReference<String>
|
||||
startedKey =
|
||||
new java.util.concurrent.atomic.AtomicReference<>();
|
||||
java.util.concurrent.atomic.AtomicReference<String>
|
||||
endedKey =
|
||||
new java.util.concurrent.atomic.AtomicReference<>();
|
||||
java.util.concurrent.atomic.AtomicReference<NodeStatus>
|
||||
endedStatus =
|
||||
new java.util.concurrent.atomic.AtomicReference<>();
|
||||
java.util.concurrent.atomic.AtomicReference<Throwable>
|
||||
endedError =
|
||||
new java.util.concurrent.atomic.AtomicReference<>();
|
||||
executor.addEventListener(
|
||||
NodeStartEvent.class,
|
||||
(event, chain) -> {
|
||||
NodeStartEvent startEvent =
|
||||
(NodeStartEvent) event;
|
||||
if ("loop".equals(
|
||||
startEvent.getNode().getId())) {
|
||||
startedKey.set(
|
||||
startEvent
|
||||
.getExecutionAttemptKey());
|
||||
}
|
||||
});
|
||||
executor.addEventListener(
|
||||
NodeEndEvent.class,
|
||||
(event, chain) -> {
|
||||
NodeEndEvent endEvent =
|
||||
(NodeEndEvent) event;
|
||||
if ("loop".equals(
|
||||
endEvent.getNode().getId())) {
|
||||
// 模拟下一轮执行已抢先覆盖可变节点状态。
|
||||
chain.updateNodeStateSafely(
|
||||
"loop",
|
||||
state -> {
|
||||
state.setExecutionAttemptKey(
|
||||
"next-attempt");
|
||||
state.setStatus(
|
||||
NodeStatus.RUNNING);
|
||||
state.setError(
|
||||
new ExceptionSummary(
|
||||
new IllegalStateException(
|
||||
"next-error")));
|
||||
return EnumSet.of(
|
||||
NodeStateField.STATUS,
|
||||
NodeStateField.ERROR,
|
||||
NodeStateField
|
||||
.EXECUTION_ATTEMPT_KEY);
|
||||
});
|
||||
endedKey.set(
|
||||
endEvent
|
||||
.getExecutionAttemptKey());
|
||||
endedStatus.set(
|
||||
endEvent.getStatus());
|
||||
endedError.set(
|
||||
endEvent.getError());
|
||||
}
|
||||
});
|
||||
|
||||
executor.execute(
|
||||
"loop-attempt-key-test",
|
||||
Collections.singletonMap(
|
||||
"times", 3));
|
||||
|
||||
Assert.assertNotNull(startedKey.get());
|
||||
Assert.assertEquals(
|
||||
startedKey.get(),
|
||||
endedKey.get());
|
||||
Assert.assertEquals(
|
||||
NodeStatus.SUCCEEDED,
|
||||
endedStatus.get());
|
||||
Assert.assertNull(
|
||||
endedError.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证普通 Iterable 只物化一次,后续循环不会从头重复遍历。
|
||||
*/
|
||||
@Test
|
||||
public void shouldMaterializeNonListIterableOnlyOnce() {
|
||||
ChainDefinition definition = createDefinition();
|
||||
ChainExecutor executor = createExecutor(definition);
|
||||
OneShotIterable iterable = new OneShotIterable();
|
||||
Map<String, Object> variables = new HashMap<>();
|
||||
variables.put("times", iterable);
|
||||
|
||||
Map<String, Object> resultMap = executor.execute("loop-progress-test", variables);
|
||||
|
||||
Assert.assertEquals(Arrays.asList("1", "2", "3"), resultMap.get("result"));
|
||||
Assert.assertEquals(1, iterable.getIteratorCount());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证超过直接索引阈值的 List 首轮物化后不再回读原集合。
|
||||
*/
|
||||
@Test
|
||||
public void shouldMaterializeListOnlyOnce() {
|
||||
ChainDefinition definition = createDefinition();
|
||||
ChainExecutor executor = createExecutor(definition);
|
||||
Integer[] values = new Integer[128];
|
||||
Arrays.fill(values, 1);
|
||||
CountingList list = new CountingList(values);
|
||||
|
||||
Map<String, Object> resultMap = executor.execute(
|
||||
"loop-list-test", Collections.singletonMap("times", list));
|
||||
|
||||
Assert.assertEquals(128, ((java.util.List<?>) resultMap.get("result")).size());
|
||||
Assert.assertTrue(
|
||||
"list input should not be re-read for every iteration",
|
||||
list.getReadCount() <= values.length * 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证循环直接消费上游已分页物化的轻量引用,不先还原完整列表。
|
||||
*/
|
||||
@Test
|
||||
public void shouldConsumePreMaterializedInputReference() {
|
||||
ChainDefinition definition = createDefinition();
|
||||
InMemoryLoopResultRepository loopRepository =
|
||||
new InMemoryLoopResultRepository();
|
||||
String resultId = "loop-reference-input";
|
||||
loopRepository.storeInput(
|
||||
resultId, Arrays.asList(10, 20, 30));
|
||||
TriggerScheduler scheduler =
|
||||
new TriggerScheduler(
|
||||
new InMemoryTriggerStore(),
|
||||
Executors.newScheduledThreadPool(2),
|
||||
Executors.newFixedThreadPool(2),
|
||||
1_000L);
|
||||
try {
|
||||
ChainExecutor executor = new ChainExecutor(
|
||||
new FixedDefinitionRepository(
|
||||
definition),
|
||||
new InMemoryChainStateRepository(),
|
||||
new InMemoryNodeStateRepository(),
|
||||
loopRepository,
|
||||
scheduler,
|
||||
ExecutionBudget.defaults());
|
||||
|
||||
Map<String, Object> resultMap =
|
||||
executor.execute(
|
||||
"loop-reference-test",
|
||||
Collections.singletonMap(
|
||||
"times",
|
||||
new LoopInputReference(
|
||||
resultId, 3)));
|
||||
|
||||
Assert.assertEquals(
|
||||
Arrays.asList("1", "2", "3"),
|
||||
resultMap.get("result"));
|
||||
} finally {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证对象数组和基础类型数组均可作为循环输入。
|
||||
*/
|
||||
@Test
|
||||
public void shouldSupportArrayInputs() {
|
||||
ChainDefinition definition = createDefinition();
|
||||
ChainExecutor executor = createExecutor(definition);
|
||||
|
||||
Map<String, Object> objectArrayResult = executor.execute(
|
||||
"loop-object-array-test",
|
||||
Collections.singletonMap("times", new String[]{"a", "b"}));
|
||||
Map<String, Object> primitiveArrayResult = executor.execute(
|
||||
"loop-primitive-array-test",
|
||||
Collections.singletonMap("times", new int[]{1, 2, 3}));
|
||||
|
||||
Assert.assertEquals(Arrays.asList("1", "2"), objectArrayResult.get("result"));
|
||||
Assert.assertEquals(
|
||||
Arrays.asList("1", "2", "3"), primitiveArrayResult.get("result"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证显式循环节点接受 300 次,并拒绝 301 次的数值输入。
|
||||
*/
|
||||
@Test
|
||||
public void shouldEnforceNumericLoopLimit() {
|
||||
ChainExecutor maximumExecutor = createExecutor(createDefinition());
|
||||
Map<String, Object> maximumResult = maximumExecutor.execute(
|
||||
"loop-maximum-test",
|
||||
Collections.singletonMap("times", Node.MAX_LOOP_COUNT));
|
||||
|
||||
Assert.assertEquals(
|
||||
Node.MAX_LOOP_COUNT,
|
||||
((java.util.List<?>) maximumResult.get("result")).size());
|
||||
|
||||
ChainExecutor exceededExecutor = createExecutor(createDefinition());
|
||||
assertLoopFailure(
|
||||
exceededExecutor,
|
||||
"loop-exceeded-test",
|
||||
Collections.singletonMap(
|
||||
"times", Node.MAX_LOOP_COUNT + 1),
|
||||
IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证未知大小 Iterable 在物化第 301 个元素前终止。
|
||||
*/
|
||||
@Test
|
||||
public void shouldStopOversizedIterableDuringMaterialization() {
|
||||
ChainExecutor executor = createExecutor(createDefinition());
|
||||
|
||||
assertLoopFailure(
|
||||
executor,
|
||||
"loop-iterable-exceeded-test",
|
||||
Collections.singletonMap(
|
||||
"times",
|
||||
new RangeIterable(
|
||||
Node.MAX_LOOP_COUNT + 1)),
|
||||
ExecutionBudgetExceededException.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证大循环累计结果不会进入每轮持久化的节点热状态。
|
||||
*/
|
||||
@Test
|
||||
public void shouldKeepNodeStateCompactWhenCollectingLargeLoopResult() {
|
||||
ChainDefinition definition = createDefinition();
|
||||
TrackingNodeStateRepository nodeStateRepository = new TrackingNodeStateRepository();
|
||||
ChainExecutor executor = new ChainExecutor(new FixedDefinitionRepository(definition),
|
||||
new InMemoryChainStateRepository(),
|
||||
nodeStateRepository);
|
||||
Map<String, Object> variables = new HashMap<>();
|
||||
variables.put("times", Node.MAX_LOOP_COUNT);
|
||||
|
||||
Map<String, Object> resultMap = executor.execute("loop-progress-test", variables);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.List<String> result = (java.util.List<String>) resultMap.get("result");
|
||||
Assert.assertEquals(Node.MAX_LOOP_COUNT, result.size());
|
||||
Assert.assertEquals("1", result.get(0));
|
||||
Assert.assertEquals(
|
||||
String.valueOf(Node.MAX_LOOP_COUNT),
|
||||
result.get(Node.MAX_LOOP_COUNT - 1));
|
||||
Assert.assertTrue("loop node hot state should remain bounded",
|
||||
nodeStateRepository.getMaxSerializedBytes() < 16 * 1024);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证循环体存在多个直属分支时,必须等待全部分支完成后再推进下一轮。
|
||||
*/
|
||||
@Test
|
||||
public void shouldWaitForAllDirectLoopBranchesBeforeAdvancing() {
|
||||
ChainDefinition definition = createMultiBranchDefinition();
|
||||
ChainExecutor executor = createExecutor(definition);
|
||||
|
||||
Map<String, Object> resultMap = executor.execute(
|
||||
"loop-multi-branch-test", Collections.singletonMap("times", 3));
|
||||
|
||||
Assert.assertEquals(Arrays.asList("a0", "a1", "a2"), resultMap.get("a"));
|
||||
Assert.assertEquals(Arrays.asList("b0", "b1", "b2"), resultMap.get("b"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建循环进度测试定义。
|
||||
*
|
||||
* @return 工作流定义
|
||||
*/
|
||||
private ChainDefinition createDefinition() {
|
||||
ChainDefinition definition = new ChainDefinition();
|
||||
definition.setId("loop-progress-test");
|
||||
|
||||
@@ -67,17 +368,115 @@ public class LoopNodeProgressContextTest {
|
||||
definition.addEdge(edge("e1", "start", "loop"));
|
||||
definition.addEdge(edge("e2", "loop", "acc"));
|
||||
definition.addEdge(edge("e3", "loop", "end"));
|
||||
return definition;
|
||||
}
|
||||
|
||||
ChainExecutor executor = new ChainExecutor(new FixedDefinitionRepository(definition),
|
||||
/**
|
||||
* 创建包含两个并行直属循环分支的定义。
|
||||
*
|
||||
* @return 工作流定义
|
||||
*/
|
||||
private ChainDefinition createMultiBranchDefinition() {
|
||||
ChainDefinition definition = new ChainDefinition();
|
||||
definition.setId("loop-multi-branch-test");
|
||||
|
||||
StartNode startNode = new StartNode();
|
||||
startNode.setId("start");
|
||||
startNode.setParameters(Collections.singletonList(inputParameter("times")));
|
||||
|
||||
LoopNode loopNode = new LoopNode();
|
||||
loopNode.setId("loop");
|
||||
Parameter loopVar = new Parameter();
|
||||
loopVar.setName("times");
|
||||
loopVar.setRef("times");
|
||||
loopVar.setRefType(RefType.REF);
|
||||
loopNode.setLoopVar(loopVar);
|
||||
|
||||
BranchNode branchA = new BranchNode("a", 0L);
|
||||
branchA.setId("branch-a");
|
||||
branchA.setParentId("loop");
|
||||
BranchNode branchB = new BranchNode("b", 20L);
|
||||
branchB.setId("branch-b");
|
||||
branchB.setParentId("loop");
|
||||
|
||||
Parameter outputA = new Parameter();
|
||||
outputA.setName("a");
|
||||
outputA.setRef("branch-a.value");
|
||||
outputA.setRefType(RefType.REF);
|
||||
Parameter outputB = new Parameter();
|
||||
outputB.setName("b");
|
||||
outputB.setRef("branch-b.value");
|
||||
outputB.setRefType(RefType.REF);
|
||||
loopNode.setOutputDefs(Arrays.asList(outputA, outputB));
|
||||
|
||||
EndNode endNode = new EndNode();
|
||||
endNode.setId("end");
|
||||
Parameter resultA = new Parameter();
|
||||
resultA.setName("a");
|
||||
resultA.setRef("loop.a");
|
||||
resultA.setRefType(RefType.REF);
|
||||
Parameter resultB = new Parameter();
|
||||
resultB.setName("b");
|
||||
resultB.setRef("loop.b");
|
||||
resultB.setRefType(RefType.REF);
|
||||
endNode.setOutputDefs(Arrays.asList(resultA, resultB));
|
||||
|
||||
definition.addNode(startNode);
|
||||
definition.addNode(loopNode);
|
||||
definition.addNode(branchA);
|
||||
definition.addNode(branchB);
|
||||
definition.addNode(endNode);
|
||||
definition.addEdge(edge("e1", "start", "loop"));
|
||||
definition.addEdge(edge("e2", "loop", "branch-a"));
|
||||
definition.addEdge(edge("e3", "loop", "branch-b"));
|
||||
definition.addEdge(edge("e4", "loop", "end"));
|
||||
return definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建使用内存状态仓储的测试执行器。
|
||||
*
|
||||
* @param definition 工作流定义
|
||||
* @return 测试执行器
|
||||
*/
|
||||
private ChainExecutor createExecutor(ChainDefinition definition) {
|
||||
return new ChainExecutor(new FixedDefinitionRepository(definition),
|
||||
new InMemoryChainStateRepository(),
|
||||
new InMemoryNodeStateRepository());
|
||||
}
|
||||
|
||||
Map<String, Object> variables = new HashMap<>();
|
||||
variables.put("times", 2);
|
||||
|
||||
Map<String, Object> resultMap = executor.execute("loop-progress-test", variables);
|
||||
|
||||
Assert.assertEquals(java.util.Arrays.asList("1", "2"), resultMap.get("result"));
|
||||
/**
|
||||
* 断言循环节点通过结束事件报告指定异常类型。
|
||||
*
|
||||
* @param executor 测试执行器
|
||||
* @param definitionId 定义 ID
|
||||
* @param variables 输入变量
|
||||
* @param expectedType 期望异常类型
|
||||
*/
|
||||
private static void assertLoopFailure(
|
||||
ChainExecutor executor,
|
||||
String definitionId,
|
||||
Map<String, Object> variables,
|
||||
Class<? extends Throwable> expectedType) {
|
||||
AtomicReference<Throwable> nodeError = new AtomicReference<>();
|
||||
executor.addEventListener(
|
||||
NodeEndEvent.class,
|
||||
(event, chain) -> {
|
||||
NodeEndEvent endEvent = (NodeEndEvent) event;
|
||||
if ("loop".equals(endEvent.getNode().getId())
|
||||
&& endEvent.getError() != null) {
|
||||
nodeError.set(endEvent.getError());
|
||||
}
|
||||
});
|
||||
try {
|
||||
executor.execute(definitionId, variables);
|
||||
Assert.fail("Expected exception: " + expectedType.getSimpleName());
|
||||
} catch (RuntimeException expected) {
|
||||
Assert.assertNotNull("Loop node error event is missing", nodeError.get());
|
||||
Assert.assertTrue(
|
||||
"Unexpected loop node error: " + nodeError.get(),
|
||||
expectedType.isInstance(nodeError.get()));
|
||||
}
|
||||
}
|
||||
|
||||
private static Parameter inputParameter(String name) {
|
||||
@@ -109,6 +508,171 @@ public class LoopNodeProgressContextTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 第二次获取迭代器时直接失败,用于识别重复遍历。
|
||||
*/
|
||||
private static class OneShotIterable implements Iterable<Integer> {
|
||||
private final AtomicInteger iteratorCount = new AtomicInteger();
|
||||
|
||||
/**
|
||||
* 获取唯一可用的迭代器。
|
||||
*
|
||||
* @return 测试迭代器
|
||||
*/
|
||||
@Override
|
||||
public Iterator<Integer> iterator() {
|
||||
if (iteratorCount.incrementAndGet() > 1) {
|
||||
throw new IllegalStateException("Iterable must not be traversed more than once");
|
||||
}
|
||||
return Arrays.asList(10, 20, 30).iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取迭代器创建次数。
|
||||
*
|
||||
* @return 迭代器创建次数
|
||||
*/
|
||||
private int getIteratorCount() {
|
||||
return iteratorCount.get();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成指定数量元素且无法提前获知大小的 Iterable。
|
||||
*/
|
||||
private static final class RangeIterable implements Iterable<Integer> {
|
||||
|
||||
private final int itemCount;
|
||||
|
||||
/**
|
||||
* 创建范围输入。
|
||||
*
|
||||
* @param itemCount 元素数量
|
||||
*/
|
||||
private RangeIterable(int itemCount) {
|
||||
this.itemCount = itemCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Iterator<Integer> iterator() {
|
||||
return new Iterator<Integer>() {
|
||||
private int index;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return index < itemCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer next() {
|
||||
if (!hasNext()) {
|
||||
throw new java.util.NoSuchElementException();
|
||||
}
|
||||
return index++;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录元素读取次数的列表。
|
||||
*/
|
||||
private static final class CountingList extends AbstractList<Integer> {
|
||||
|
||||
private final java.util.List<Integer> values;
|
||||
private final AtomicInteger readCount = new AtomicInteger();
|
||||
|
||||
/**
|
||||
* 创建计数列表。
|
||||
*
|
||||
* @param values 列表元素
|
||||
*/
|
||||
private CountingList(Integer... values) {
|
||||
this.values = Arrays.asList(values);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Integer get(int index) {
|
||||
readCount.incrementAndGet();
|
||||
return values.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int size() {
|
||||
return values.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元素读取次数。
|
||||
*
|
||||
* @return 元素读取次数
|
||||
*/
|
||||
private int getReadCount() {
|
||||
return readCount.get();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录节点状态序列化体积的测试仓储。
|
||||
*/
|
||||
private static final class TrackingNodeStateRepository implements NodeStateRepository {
|
||||
|
||||
private final InMemoryNodeStateRepository delegate = new InMemoryNodeStateRepository();
|
||||
private final AtomicInteger maxSerializedBytes = new AtomicInteger();
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public NodeState load(String instanceId, String nodeId) {
|
||||
return delegate.load(instanceId, nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean tryUpdate(NodeState newState, EnumSet<NodeStateField> fields, long chainStateVersion) {
|
||||
maxSerializedBytes.accumulateAndGet(serializedSize(newState), Math::max);
|
||||
return delegate.tryUpdate(newState, fields, chainStateVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取观测到的最大序列化字节数。
|
||||
*
|
||||
* @return 最大序列化字节数
|
||||
*/
|
||||
private int getMaxSerializedBytes() {
|
||||
return maxSerializedBytes.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算节点状态的 Java 序列化字节数。
|
||||
*
|
||||
* @param state 节点状态
|
||||
* @return 序列化字节数
|
||||
*/
|
||||
private int serializedSize(NodeState state) {
|
||||
try (ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
ObjectOutputStream objectOutput = new ObjectOutputStream(output)) {
|
||||
objectOutput.writeObject(state);
|
||||
objectOutput.flush();
|
||||
return output.size();
|
||||
} catch (IOException error) {
|
||||
throw new IllegalStateException("Failed to serialize node state", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 每一轮都读取父循环节点上一轮的 current,再计算新的结果。
|
||||
*/
|
||||
@@ -142,4 +706,31 @@ public class LoopNodeProgressContextTest {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出当前循环序号的测试分支。
|
||||
*/
|
||||
private static class BranchNode extends BaseNode {
|
||||
private final String prefix;
|
||||
private final long delayMs;
|
||||
|
||||
private BranchNode(String prefix, long delayMs) {
|
||||
this.prefix = prefix;
|
||||
this.delayMs = delayMs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
if (delayMs > 0) {
|
||||
try {
|
||||
Thread.sleep(delayMs);
|
||||
} catch (InterruptedException error) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Branch execution interrupted", error);
|
||||
}
|
||||
}
|
||||
Object index = chain.getState().resolveValue("loop.index");
|
||||
return Collections.singletonMap("value", prefix + index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.easyagents.flow.core.test;
|
||||
|
||||
import com.easyagents.flow.core.chain.repository.InMemoryLoopResultRepository;
|
||||
import com.easyagents.flow.core.chain.repository.LoopInputReference;
|
||||
import com.easyagents.flow.core.chain.repository.LoopResultReference;
|
||||
import com.easyagents.flow.core.chain.runtime.ExecutionBudgetExceededException;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Iterator;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* 循环结果引用批量还原回归测试。
|
||||
*/
|
||||
public class LoopResultReferenceResolverTest {
|
||||
|
||||
/**
|
||||
* 验证同一循环的多个输出只触发一次仓储加载。
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void shouldBatchOutputsFromSameLoopResult() {
|
||||
CountingLoopResultRepository repository = new CountingLoopResultRepository();
|
||||
repository.append("result-1", 0, Map.of("a", "a0", "b", "b0"));
|
||||
repository.append("result-1", 1, Map.of("a", "a1", "b", "b1"));
|
||||
Map<String, Object> value = new LinkedHashMap<>();
|
||||
value.put("a", new LoopResultReference("result-1", 2, "a"));
|
||||
value.put("nested", List.of(new LoopResultReference("result-1", 2, "b")));
|
||||
|
||||
Map<String, Object> resolved =
|
||||
(Map<String, Object>) repository.resolveReferences(value);
|
||||
|
||||
Assert.assertEquals(Arrays.asList("a0", "a1"), resolved.get("a"));
|
||||
Assert.assertEquals(
|
||||
Arrays.asList("b0", "b1"),
|
||||
((List<Object>) resolved.get("nested")).get(0));
|
||||
Assert.assertEquals(1, repository.getLoadCount());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证无限 Iterable 在达到预算后立即停止物化。
|
||||
*/
|
||||
@Test
|
||||
public void shouldStopInfiniteInputAtIterationBudget() {
|
||||
InMemoryLoopResultRepository repository = new InMemoryLoopResultRepository();
|
||||
AtomicInteger reads = new AtomicInteger();
|
||||
Iterable<Integer> infinite = () -> new Iterator<Integer>() {
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer next() {
|
||||
return reads.incrementAndGet();
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
repository.storeInput("bounded-input", infinite, 3L);
|
||||
Assert.fail("infinite input should exceed the iteration budget");
|
||||
} catch (ExecutionBudgetExceededException expected) {
|
||||
Assert.assertTrue(expected.getMessage().contains("more than 3"));
|
||||
}
|
||||
|
||||
Assert.assertEquals(3, reads.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证外置循环输入在业务读取边界仍还原为原顺序列表。
|
||||
*/
|
||||
@Test
|
||||
public void shouldResolveExternalizedLoopInput() {
|
||||
InMemoryLoopResultRepository repository =
|
||||
new InMemoryLoopResultRepository();
|
||||
String resultId = "input-reference";
|
||||
Assert.assertEquals(
|
||||
3,
|
||||
repository.storeInput(
|
||||
resultId,
|
||||
List.of("first", "second", "third")));
|
||||
|
||||
Object resolved = repository.resolveReferences(
|
||||
new LoopInputReference(resultId, 3));
|
||||
|
||||
Assert.assertEquals(
|
||||
List.of("first", "second", "third"),
|
||||
resolved);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录加载次数的循环结果仓储。
|
||||
*/
|
||||
private static final class CountingLoopResultRepository
|
||||
extends InMemoryLoopResultRepository {
|
||||
|
||||
private final AtomicInteger loadCount = new AtomicInteger();
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> load(
|
||||
String resultId, int iterationCount, List<String> outputNames) {
|
||||
loadCount.incrementAndGet();
|
||||
return super.load(resultId, iterationCount, outputNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取仓储加载次数。
|
||||
*
|
||||
* @return 加载次数
|
||||
*/
|
||||
private int getLoadCount() {
|
||||
return loadCount.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.easyagents.flow.core.test;
|
||||
|
||||
import com.easyagents.flow.core.util.IoBulkhead;
|
||||
import com.easyagents.flow.core.util.OkHttpClientUtil;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* {@link OkHttpClientUtil} 共享连接池回归测试。
|
||||
*/
|
||||
public class OkHttpClientUtilTest {
|
||||
|
||||
/**
|
||||
* 验证默认客户端跨节点调用复用同一实例。
|
||||
*/
|
||||
@Test
|
||||
public void shouldReuseDefaultClientInstance() {
|
||||
OkHttpClient first = OkHttpClientUtil.buildDefaultClient();
|
||||
OkHttpClient second = OkHttpClientUtil.buildDefaultClient();
|
||||
|
||||
Assert.assertSame(first, second);
|
||||
Assert.assertSame(first.connectionPool(), second.connectionPool());
|
||||
Assert.assertSame(first.dispatcher(), second.dispatcher());
|
||||
Assert.assertEquals(
|
||||
1L,
|
||||
first.interceptors().stream()
|
||||
.filter(interceptor -> interceptor.getClass().getSimpleName()
|
||||
.equals("IoBulkheadInterceptor"))
|
||||
.count());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证非幂等请求客户端关闭底层隐式重试且继续复用连接资源。
|
||||
*/
|
||||
@Test
|
||||
public void shouldReuseNoRetryClientAndConnectionPool() {
|
||||
OkHttpClient defaultClient = OkHttpClientUtil.buildDefaultClient();
|
||||
OkHttpClient first = OkHttpClientUtil.buildNoRetryClient();
|
||||
OkHttpClient second = OkHttpClientUtil.buildNoRetryClient();
|
||||
|
||||
Assert.assertSame(first, second);
|
||||
Assert.assertFalse(first.retryOnConnectionFailure());
|
||||
Assert.assertSame(defaultClient.connectionPool(), first.connectionPool());
|
||||
Assert.assertSame(defaultClient.dispatcher(), first.dispatcher());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证单目标并发为一时一次 HTTP 请求只申请一次许可且不会自锁。
|
||||
*
|
||||
* @throws Exception 启动本地 HTTP 服务或请求失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldAcquireOnePermitPerHttpRequest() throws Exception {
|
||||
IoBulkhead.Settings restricted =
|
||||
new IoBulkhead.Settings(1, 1, Duration.ofMillis(200), 16);
|
||||
IoBulkhead.configure(
|
||||
restricted,
|
||||
new IoBulkhead.Settings(32, 8, Duration.ofSeconds(1), 512),
|
||||
new IoBulkhead.Settings(24, 12, Duration.ofSeconds(2), 256),
|
||||
new IoBulkhead.Settings(8, 4, Duration.ofSeconds(2), 128),
|
||||
new IoBulkhead.Settings(8, 4, Duration.ofSeconds(2), 1_024));
|
||||
HttpServer server = HttpServer.create(
|
||||
new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext("/", exchange -> {
|
||||
byte[] body = "ok".getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
exchange.sendResponseHeaders(200, body.length);
|
||||
exchange.getResponseBody().write(body);
|
||||
exchange.close();
|
||||
});
|
||||
server.start();
|
||||
try {
|
||||
long acquiredBefore = IoBulkhead.shared()
|
||||
.snapshot()
|
||||
.acquiredCount();
|
||||
Request request = new Request.Builder()
|
||||
.url("http://127.0.0.1:"
|
||||
+ server.getAddress().getPort()
|
||||
+ "/")
|
||||
.build();
|
||||
try (Response response = OkHttpClientUtil
|
||||
.buildNoRetryClient()
|
||||
.newCall(request)
|
||||
.execute()) {
|
||||
Assert.assertEquals(200, response.code());
|
||||
}
|
||||
Assert.assertEquals(
|
||||
acquiredBefore + 1,
|
||||
IoBulkhead.shared().snapshot().acquiredCount());
|
||||
Assert.assertEquals(
|
||||
0L,
|
||||
IoBulkhead.shared().snapshot().inFlightCount());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
restoreDefaultBulkheads();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复测试进程的宽松默认隔离配置,避免污染其他测试。
|
||||
*/
|
||||
private void restoreDefaultBulkheads() {
|
||||
IoBulkhead.configure(
|
||||
new IoBulkhead.Settings(64, 16, Duration.ofSeconds(1), 1_024),
|
||||
new IoBulkhead.Settings(32, 8, Duration.ofSeconds(1), 512),
|
||||
new IoBulkhead.Settings(24, 12, Duration.ofSeconds(2), 256),
|
||||
new IoBulkhead.Settings(8, 4, Duration.ofSeconds(2), 128),
|
||||
new IoBulkhead.Settings(8, 4, Duration.ofSeconds(2), 1_024));
|
||||
}
|
||||
}
|
||||
@@ -44,4 +44,66 @@ public class TextTemplatePathTest {
|
||||
|
||||
Assert.assertEquals("value=", result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证分层上下文保持后层覆盖前层的原有优先级。
|
||||
*/
|
||||
@Test
|
||||
public void shouldResolveLayeredContextUsingLastMapPrecedence() {
|
||||
Map<String, Object> memory = new HashMap<>();
|
||||
memory.put("name", "memory");
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
parameters.put("name", "parameter");
|
||||
|
||||
String result = TextTemplate.of("{{name}}")
|
||||
.formatToString(Arrays.asList(memory, parameters));
|
||||
|
||||
Assert.assertEquals("parameter", result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证分层上下文支持跨层兜底并保持 JSON 转义。
|
||||
*/
|
||||
@Test
|
||||
public void shouldResolveFallbackAcrossLayersWithJsonEscaping() {
|
||||
Map<String, Object> memory = new HashMap<>();
|
||||
memory.put("fallback", "a\"b");
|
||||
|
||||
String result = TextTemplate.of("{\"value\":\"{{missing ?? fallback}}\"}")
|
||||
.formatToString(Arrays.asList(memory, Collections.emptyMap()), true);
|
||||
|
||||
Assert.assertEquals("{\"value\":\"a\\\"b\"}", result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 后层拥有顶级键时,空对象应完整遮蔽前层同名对象。
|
||||
*/
|
||||
@Test
|
||||
public void shouldPreservePutAllShadowingForNestedObject() {
|
||||
Map<String, Object> memory = new HashMap<>();
|
||||
memory.put("user", Collections.singletonMap("name", "legacy"));
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
parameters.put("user", Collections.emptyMap());
|
||||
|
||||
String result = TextTemplate.of("{{user.name ?? \"fallback\"}}")
|
||||
.formatToString(Arrays.asList(memory, parameters));
|
||||
|
||||
Assert.assertEquals("fallback", result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 后层显式 null 应遮蔽前层同名对象并进入模板兜底。
|
||||
*/
|
||||
@Test
|
||||
public void shouldPreservePutAllShadowingForExplicitNull() {
|
||||
Map<String, Object> memory = new HashMap<>();
|
||||
memory.put("user", Collections.singletonMap("name", "legacy"));
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
parameters.put("user", null);
|
||||
|
||||
String result = TextTemplate.of("{{user.name ?? \"fallback\"}}")
|
||||
.formatToString(Arrays.asList(memory, parameters));
|
||||
|
||||
Assert.assertEquals("fallback", result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,973 @@
|
||||
package com.easyagents.flow.core.test;
|
||||
|
||||
import com.easyagents.flow.core.chain.runtime.InMemoryTriggerStore;
|
||||
import com.easyagents.flow.core.chain.runtime.NonRetryableTriggerException;
|
||||
import com.easyagents.flow.core.chain.runtime.RetryableTriggerException;
|
||||
import com.easyagents.flow.core.chain.runtime.Trigger;
|
||||
import com.easyagents.flow.core.chain.runtime.TriggerScheduler;
|
||||
import com.easyagents.flow.core.chain.runtime.TriggerStore;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.SynchronousQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* {@link TriggerScheduler} 认领、确认和失败释放语义回归测试。
|
||||
*/
|
||||
public class TriggerSchedulerReliabilityTest {
|
||||
|
||||
/**
|
||||
* 验证消费者尚未注册时主动触发不会丢失待执行任务。
|
||||
*/
|
||||
@Test
|
||||
public void shouldKeepTriggerPendingWhenConsumerIsUnavailable() {
|
||||
InMemoryTriggerStore store = new InMemoryTriggerStore();
|
||||
Trigger trigger = futureTrigger("consumer-unavailable");
|
||||
store.save(trigger);
|
||||
TriggerScheduler scheduler = scheduler(store);
|
||||
try {
|
||||
Assert.assertFalse(scheduler.fire(trigger.getId()));
|
||||
Assert.assertNotNull(store.find(trigger.getId()));
|
||||
} finally {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证执行失败后触发器被释放,并能再次认领成功。
|
||||
*
|
||||
* @throws Exception 等待异步执行被中断时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldReleaseFailedTriggerForRetry() throws Exception {
|
||||
InMemoryTriggerStore store = new InMemoryTriggerStore();
|
||||
Trigger trigger = futureTrigger("retry-after-failure");
|
||||
store.save(trigger);
|
||||
TriggerScheduler scheduler = scheduler(store);
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
CountDownLatch firstAttempt = new CountDownLatch(1);
|
||||
CountDownLatch succeeded = new CountDownLatch(1);
|
||||
scheduler.registerConsumer((current, worker) -> {
|
||||
if (attempts.incrementAndGet() == 1) {
|
||||
firstAttempt.countDown();
|
||||
throw new IllegalStateException("expected first failure");
|
||||
}
|
||||
succeeded.countDown();
|
||||
});
|
||||
|
||||
try {
|
||||
Assert.assertTrue(scheduler.fire(trigger.getId()));
|
||||
Assert.assertTrue(firstAttempt.await(2, TimeUnit.SECONDS));
|
||||
awaitPending(store, trigger.getId());
|
||||
Assert.assertTrue(scheduler.fire(trigger.getId()));
|
||||
Assert.assertTrue(succeeded.await(2, TimeUnit.SECONDS));
|
||||
Assert.assertEquals(2, attempts.get());
|
||||
} finally {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证并发认领同一触发器时只有一个消费者获得执行权。
|
||||
*
|
||||
* @throws Exception 并发任务等待失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldDispatchClaimedTriggerOnlyOnce() throws Exception {
|
||||
InMemoryTriggerStore store = new InMemoryTriggerStore();
|
||||
Trigger trigger = futureTrigger("single-claim");
|
||||
store.save(trigger);
|
||||
TriggerScheduler scheduler = scheduler(store);
|
||||
CountDownLatch consumed = new CountDownLatch(1);
|
||||
AtomicInteger executions = new AtomicInteger();
|
||||
scheduler.registerConsumer((current, worker) -> {
|
||||
executions.incrementAndGet();
|
||||
consumed.countDown();
|
||||
});
|
||||
ExecutorService callers = Executors.newFixedThreadPool(2);
|
||||
|
||||
try {
|
||||
List<java.util.concurrent.Future<Boolean>> results = callers.invokeAll(Arrays.asList(
|
||||
() -> scheduler.fire(trigger.getId()),
|
||||
() -> scheduler.fire(trigger.getId())));
|
||||
int accepted = 0;
|
||||
for (java.util.concurrent.Future<Boolean> result : results) {
|
||||
if (result.get()) {
|
||||
accepted++;
|
||||
}
|
||||
}
|
||||
Assert.assertEquals(1, accepted);
|
||||
Assert.assertTrue(consumed.await(2, TimeUnit.SECONDS));
|
||||
Assert.assertEquals(1, executions.get());
|
||||
} finally {
|
||||
callers.shutdownNow();
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证缺少认领对象时旧 ID 接口不会静默执行不安全确认。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectClaimMutationWithoutClaimedTrigger() {
|
||||
InMemoryTriggerStore store = new InMemoryTriggerStore();
|
||||
|
||||
try {
|
||||
store.renewClaim("unsafe-id-only", 1000L);
|
||||
Assert.fail("ID-only renewal must be rejected");
|
||||
} catch (UnsupportedOperationException expected) {
|
||||
Assert.assertTrue(expected.getMessage().contains("token"));
|
||||
}
|
||||
|
||||
try {
|
||||
store.acknowledge("unsafe-id-only");
|
||||
Assert.fail("ID-only acknowledge must be rejected");
|
||||
} catch (UnsupportedOperationException expected) {
|
||||
Assert.assertTrue(expected.getMessage().contains("trigger"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证不可重试错误会移出待执行集合,避免形成永久热重放。
|
||||
*
|
||||
* @throws Exception 等待异步执行被中断时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldDeadLetterNonRetryableTrigger() throws Exception {
|
||||
InMemoryTriggerStore store = new InMemoryTriggerStore();
|
||||
Trigger trigger = futureTrigger("non-retryable");
|
||||
store.save(trigger);
|
||||
TriggerScheduler scheduler = scheduler(store);
|
||||
CountDownLatch attempted = new CountDownLatch(1);
|
||||
scheduler.registerConsumer((current, worker) -> {
|
||||
attempted.countDown();
|
||||
throw new NonRetryableTriggerException("invalid definition");
|
||||
});
|
||||
|
||||
try {
|
||||
Assert.assertTrue(scheduler.fire(trigger.getId()));
|
||||
Assert.assertTrue(attempted.await(2, TimeUnit.SECONDS));
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2);
|
||||
while (store.find(trigger.getId()) != null && System.nanoTime() < deadline) {
|
||||
Thread.sleep(10L);
|
||||
}
|
||||
Assert.assertNull(store.find(trigger.getId()));
|
||||
} finally {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证瞬时基础设施冲突即使超过普通投递上限也继续保留,避免幂等 owner 存活期内误死信。
|
||||
*
|
||||
* @throws Exception 等待异步执行被中断时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldNotDeadLetterTransientContention()
|
||||
throws Exception {
|
||||
InMemoryTriggerStore store = new InMemoryTriggerStore();
|
||||
Trigger trigger = futureTrigger("transient-contention");
|
||||
trigger.setDeliveryAttempt(100);
|
||||
store.save(trigger);
|
||||
TriggerScheduler scheduler = scheduler(store);
|
||||
CountDownLatch attempted = new CountDownLatch(1);
|
||||
scheduler.registerConsumer((current, worker) -> {
|
||||
attempted.countDown();
|
||||
throw new RetryableTriggerException(
|
||||
"operation still owned", null);
|
||||
});
|
||||
|
||||
try {
|
||||
Assert.assertTrue(scheduler.fire(trigger.getId()));
|
||||
Assert.assertTrue(attempted.await(2, TimeUnit.SECONDS));
|
||||
awaitPending(store, trigger.getId());
|
||||
Assert.assertEquals(
|
||||
101,
|
||||
store.find(trigger.getId())
|
||||
.getDeliveryAttempt());
|
||||
} finally {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证业务终态尚未持久化时不得先移除触发器。
|
||||
*
|
||||
* @throws Exception 等待异步执行被中断时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldRetryTerminalConvergenceBeforeDeadLetter()
|
||||
throws Exception {
|
||||
InMemoryTriggerStore store = new InMemoryTriggerStore();
|
||||
Trigger trigger = futureTrigger(
|
||||
"terminal-convergence-retry");
|
||||
store.save(trigger);
|
||||
TriggerScheduler scheduler = scheduler(store);
|
||||
AtomicInteger convergenceAttempts = new AtomicInteger();
|
||||
AtomicInteger executions = new AtomicInteger();
|
||||
CountDownLatch consumed = new CountDownLatch(1);
|
||||
scheduler.registerConsumer((current, worker) -> {
|
||||
executions.incrementAndGet();
|
||||
consumed.countDown();
|
||||
throw new NonRetryableTriggerException(
|
||||
"invalid definition");
|
||||
});
|
||||
scheduler.registerFailureListener((current, failure) ->
|
||||
convergenceAttempts.incrementAndGet() >= 2);
|
||||
|
||||
try {
|
||||
Assert.assertTrue(scheduler.fire(trigger.getId()));
|
||||
awaitPending(store, trigger.getId());
|
||||
Assert.assertNotNull(store.find(trigger.getId()));
|
||||
Assert.assertTrue(scheduler.fire(trigger.getId()));
|
||||
Assert.assertTrue(consumed.await(2, TimeUnit.SECONDS));
|
||||
long deadline = System.nanoTime()
|
||||
+ TimeUnit.SECONDS.toNanos(2);
|
||||
while ((store.find(trigger.getId()) != null
|
||||
|| convergenceAttempts.get() < 2)
|
||||
&& System.nanoTime() < deadline) {
|
||||
Thread.sleep(10L);
|
||||
}
|
||||
Assert.assertNull(store.find(trigger.getId()));
|
||||
Assert.assertEquals(2, convergenceAttempts.get());
|
||||
Assert.assertEquals(1, executions.get());
|
||||
} finally {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证死信写入失败后只重放终态协议,不重复执行业务消费者。
|
||||
*
|
||||
* @throws Exception 等待异步执行被中断时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldResumeDeadLetterFinalizationWithoutBusinessReplay()
|
||||
throws Exception {
|
||||
FailingTerminalStore store =
|
||||
new FailingTerminalStore(
|
||||
TerminalOperation.DEAD_LETTER);
|
||||
Trigger trigger = futureTrigger(
|
||||
"dead-letter-finalization-retry");
|
||||
store.save(trigger);
|
||||
TriggerScheduler scheduler = scheduler(store);
|
||||
AtomicInteger executions = new AtomicInteger();
|
||||
AtomicInteger convergenceAttempts =
|
||||
new AtomicInteger();
|
||||
CountDownLatch convergenceCompleted =
|
||||
new CountDownLatch(2);
|
||||
scheduler.registerConsumer((current, worker) -> {
|
||||
executions.incrementAndGet();
|
||||
throw new NonRetryableTriggerException(
|
||||
"invalid definition");
|
||||
});
|
||||
scheduler.registerFailureListener(
|
||||
(current, failure) -> {
|
||||
convergenceAttempts.incrementAndGet();
|
||||
convergenceCompleted.countDown();
|
||||
return true;
|
||||
});
|
||||
|
||||
try {
|
||||
Assert.assertTrue(scheduler.fire(
|
||||
trigger.getId()));
|
||||
Assert.assertTrue(store.terminalAttempted.await(
|
||||
2, TimeUnit.SECONDS));
|
||||
awaitPending(store, trigger.getId());
|
||||
Assert.assertTrue(store.find(trigger.getId())
|
||||
.isDeadLetterPending());
|
||||
|
||||
store.fail = false;
|
||||
Assert.assertTrue(scheduler.fire(
|
||||
trigger.getId()));
|
||||
Assert.assertTrue(convergenceCompleted.await(
|
||||
2, TimeUnit.SECONDS));
|
||||
long deadline = System.nanoTime()
|
||||
+ TimeUnit.SECONDS.toNanos(2);
|
||||
while (store.find(trigger.getId()) != null
|
||||
&& System.nanoTime() < deadline) {
|
||||
Thread.sleep(10L);
|
||||
}
|
||||
|
||||
Assert.assertNull(store.find(trigger.getId()));
|
||||
Assert.assertEquals(1, executions.get());
|
||||
Assert.assertEquals(
|
||||
2, convergenceAttempts.get());
|
||||
} finally {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证旧 owner 失去 claim 后不能提交业务失败终态或移动新 owner 的触发器。
|
||||
*
|
||||
* @throws Exception 等待异步执行被中断时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectDeadLetterFinalizationAfterClaimTransfer()
|
||||
throws Exception {
|
||||
LostClaimStore store = new LostClaimStore();
|
||||
Trigger trigger = futureTrigger(
|
||||
"claim-transferred");
|
||||
store.save(trigger);
|
||||
TriggerScheduler scheduler = scheduler(store);
|
||||
AtomicInteger convergenceAttempts =
|
||||
new AtomicInteger();
|
||||
CountDownLatch executed = new CountDownLatch(1);
|
||||
scheduler.registerConsumer((current, worker) -> {
|
||||
executed.countDown();
|
||||
throw new NonRetryableTriggerException(
|
||||
"stale owner");
|
||||
});
|
||||
scheduler.registerFailureListener(
|
||||
(current, failure) -> {
|
||||
convergenceAttempts.incrementAndGet();
|
||||
return true;
|
||||
});
|
||||
|
||||
try {
|
||||
Assert.assertTrue(scheduler.fire(
|
||||
trigger.getId()));
|
||||
Assert.assertTrue(executed.await(
|
||||
2, TimeUnit.SECONDS));
|
||||
Assert.assertTrue(store.finalizationRejected.await(
|
||||
2, TimeUnit.SECONDS));
|
||||
Assert.assertEquals(
|
||||
0, convergenceAttempts.get());
|
||||
Assert.assertEquals(
|
||||
0, store.deadLetterAttempts.get());
|
||||
} finally {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证远期触发器只在固定容量内建立精确定时,溢出任务继续由持久仓储保留。
|
||||
*
|
||||
* @throws Exception 反射读取调度状态失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldBoundFutureTriggerLocalSchedules() throws Exception {
|
||||
InMemoryTriggerStore store = new InMemoryTriggerStore();
|
||||
TriggerScheduler scheduler = scheduler(store);
|
||||
try {
|
||||
for (int index = 0; index < 2000; index++) {
|
||||
scheduler.schedule(nearFutureTrigger(
|
||||
"future-" + index,
|
||||
TimeUnit.SECONDS.toMillis(50)));
|
||||
}
|
||||
Field field = TriggerScheduler.class.getDeclaredField(
|
||||
"scheduledFutures");
|
||||
field.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, ?> localFutures =
|
||||
(Map<String, ?>) field.get(scheduler);
|
||||
Assert.assertEquals(1024, localFutures.size());
|
||||
Assert.assertEquals(2000, store.findAllPending().size());
|
||||
} finally {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证并发登记远期触发器时本地定时表仍严格受容量上限约束。
|
||||
*
|
||||
* @throws Exception 并发调度或反射读取状态失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldBoundConcurrentFutureTriggerLocalSchedules()
|
||||
throws Exception {
|
||||
InMemoryTriggerStore store = new InMemoryTriggerStore();
|
||||
TriggerScheduler scheduler = scheduler(store);
|
||||
ExecutorService callers =
|
||||
Executors.newFixedThreadPool(16);
|
||||
List<java.util.concurrent.Callable<Void>> tasks =
|
||||
new ArrayList<>();
|
||||
for (int index = 0; index < 2000; index++) {
|
||||
int triggerIndex = index;
|
||||
tasks.add(() -> {
|
||||
scheduler.schedule(nearFutureTrigger(
|
||||
"concurrent-future-" + triggerIndex,
|
||||
TimeUnit.SECONDS.toMillis(50)));
|
||||
return null;
|
||||
});
|
||||
}
|
||||
try {
|
||||
callers.invokeAll(tasks).forEach(result -> {
|
||||
try {
|
||||
result.get();
|
||||
} catch (Exception error) {
|
||||
throw new AssertionError(
|
||||
"concurrent schedule failed",
|
||||
error);
|
||||
}
|
||||
});
|
||||
Field field = TriggerScheduler.class.getDeclaredField(
|
||||
"scheduledFutures");
|
||||
field.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, ?> localFutures =
|
||||
(Map<String, ?>) field.get(scheduler);
|
||||
Assert.assertEquals(1024, localFutures.size());
|
||||
Assert.assertEquals(
|
||||
2000,
|
||||
store.findAllPending().size());
|
||||
} finally {
|
||||
callers.shutdownNow();
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证高并发零延迟触发完成后 Future 与时间索引同时清空。
|
||||
*
|
||||
* @throws Exception 等待任务完成或反射读取索引失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldNotLeakImmediateTriggerTimeIndex()
|
||||
throws Exception {
|
||||
InMemoryTriggerStore store =
|
||||
new InMemoryTriggerStore();
|
||||
TriggerScheduler scheduler =
|
||||
scheduler(store);
|
||||
int triggerCount = 1000;
|
||||
CountDownLatch consumed =
|
||||
new CountDownLatch(triggerCount);
|
||||
scheduler.registerConsumer(
|
||||
(current, worker) ->
|
||||
consumed.countDown());
|
||||
|
||||
try {
|
||||
for (int index = 0;
|
||||
index < triggerCount;
|
||||
index++) {
|
||||
scheduler.schedule(
|
||||
dueTrigger(
|
||||
"immediate-" + index));
|
||||
}
|
||||
Assert.assertTrue(
|
||||
consumed.await(
|
||||
5L,
|
||||
TimeUnit.SECONDS));
|
||||
awaitLocalScheduleIndexEmpty(
|
||||
scheduler,
|
||||
"scheduledFutures");
|
||||
awaitLocalScheduleIndexEmpty(
|
||||
scheduler,
|
||||
"scheduledTriggerTimes");
|
||||
} finally {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证近未来触发器按目标时间执行,不附加完整扫描周期。
|
||||
*
|
||||
* @throws Exception 等待异步触发被中断时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldDispatchFutureTriggerBeforeNextScan() throws Exception {
|
||||
InMemoryTriggerStore store = new InMemoryTriggerStore();
|
||||
TriggerScheduler scheduler = scheduler(store);
|
||||
CountDownLatch consumed = new CountDownLatch(1);
|
||||
scheduler.registerConsumer((current, worker) ->
|
||||
consumed.countDown());
|
||||
Trigger trigger = new Trigger();
|
||||
trigger.setId("future-exact");
|
||||
trigger.setTriggerAt(
|
||||
System.currentTimeMillis() + 200L);
|
||||
|
||||
try {
|
||||
scheduler.schedule(trigger);
|
||||
Assert.assertTrue(consumed.await(
|
||||
700L, TimeUnit.MILLISECONDS));
|
||||
} finally {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证较晚任务占满本地定时容量时,更早到期的任务仍获得精确定时槽位。
|
||||
*
|
||||
* @throws Exception 等待异步触发被中断时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldPreferNearTriggerWhenLocalScheduleIsFull()
|
||||
throws Exception {
|
||||
InMemoryTriggerStore store =
|
||||
new InMemoryTriggerStore();
|
||||
TriggerScheduler scheduler = scheduler(store);
|
||||
CountDownLatch consumed = new CountDownLatch(1);
|
||||
scheduler.registerConsumer((current, worker) -> {
|
||||
if ("near-deadline".equals(
|
||||
current.getId())) {
|
||||
consumed.countDown();
|
||||
}
|
||||
});
|
||||
try {
|
||||
for (int index = 0; index < 1024; index++) {
|
||||
scheduler.schedule(nearFutureTrigger(
|
||||
"later-" + index,
|
||||
TimeUnit.SECONDS.toMillis(50)));
|
||||
}
|
||||
scheduler.schedule(nearFutureTrigger(
|
||||
"near-deadline", 200L));
|
||||
|
||||
Assert.assertTrue(
|
||||
"near trigger should replace a later local timer",
|
||||
consumed.await(
|
||||
700L,
|
||||
TimeUnit.MILLISECONDS));
|
||||
Assert.assertNull(
|
||||
store.find("near-deadline"));
|
||||
Assert.assertEquals(
|
||||
1024,
|
||||
store.findAllPending().size());
|
||||
} finally {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证两个调度器并发登记同一稳定入口时,仓储只创建一份待执行触发器。
|
||||
*
|
||||
* @throws Exception 并发调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldAtomicallyScheduleStableTriggerOnce()
|
||||
throws Exception {
|
||||
InMemoryTriggerStore store =
|
||||
new InMemoryTriggerStore();
|
||||
TriggerScheduler first = scheduler(store);
|
||||
TriggerScheduler second = scheduler(store);
|
||||
ExecutorService callers =
|
||||
Executors.newFixedThreadPool(2);
|
||||
CountDownLatch ready = new CountDownLatch(2);
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
try {
|
||||
java.util.concurrent.Future<Trigger>
|
||||
firstResult = callers.submit(() -> {
|
||||
ready.countDown();
|
||||
start.await();
|
||||
return first.scheduleIfAbsent(
|
||||
futureTrigger(
|
||||
"stable-entry"));
|
||||
});
|
||||
java.util.concurrent.Future<Trigger>
|
||||
secondResult = callers.submit(() -> {
|
||||
ready.countDown();
|
||||
start.await();
|
||||
return second.scheduleIfAbsent(
|
||||
futureTrigger(
|
||||
"stable-entry"));
|
||||
});
|
||||
Assert.assertTrue(
|
||||
ready.await(1L, TimeUnit.SECONDS));
|
||||
start.countDown();
|
||||
for (java.util.concurrent.Future<Trigger> result
|
||||
: Arrays.asList(
|
||||
firstResult,
|
||||
secondResult)) {
|
||||
Assert.assertEquals(
|
||||
"stable-entry",
|
||||
result.get().getId());
|
||||
}
|
||||
Assert.assertEquals(
|
||||
1,
|
||||
store.findAllPending().size());
|
||||
} finally {
|
||||
start.countDown();
|
||||
callers.shutdownNow();
|
||||
first.shutdown();
|
||||
second.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证工作线程容量暂时耗尽后,持久化触发器可由后续扫描自动恢复执行。
|
||||
*
|
||||
* @throws Exception 等待异步执行被中断时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldRescheduleTriggerAfterDispatchCapacityRecovers() throws Exception {
|
||||
InMemoryTriggerStore store = new InMemoryTriggerStore();
|
||||
ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(2);
|
||||
ThreadPoolExecutor worker = new ThreadPoolExecutor(
|
||||
1,
|
||||
1,
|
||||
0L,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new SynchronousQueue<>());
|
||||
TriggerScheduler scheduler =
|
||||
new TriggerScheduler(store, scheduledExecutor, worker, 1000L);
|
||||
CountDownLatch firstStarted = new CountDownLatch(1);
|
||||
CountDownLatch releaseFirst = new CountDownLatch(1);
|
||||
CountDownLatch secondConsumed = new CountDownLatch(1);
|
||||
scheduler.registerConsumer((current, executor) -> {
|
||||
if ("capacity-first".equals(current.getId())) {
|
||||
firstStarted.countDown();
|
||||
try {
|
||||
releaseFirst.await(2, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("test consumer interrupted", exception);
|
||||
}
|
||||
} else if ("capacity-second".equals(current.getId())) {
|
||||
secondConsumed.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
scheduler.schedule(dueTrigger("capacity-first"));
|
||||
Assert.assertTrue(firstStarted.await(2, TimeUnit.SECONDS));
|
||||
scheduler.schedule(dueTrigger("capacity-second"));
|
||||
Thread.sleep(100L);
|
||||
Assert.assertNotNull(store.find("capacity-second"));
|
||||
|
||||
releaseFirst.countDown();
|
||||
Assert.assertTrue(
|
||||
"pending trigger should be rescheduled after capacity recovers",
|
||||
secondConsumed.await(3, TimeUnit.SECONDS));
|
||||
} finally {
|
||||
releaseFirst.countDown();
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证本地调度表被已完成占位填满后,会先清扫并继续接收新任务。
|
||||
*
|
||||
* @throws Exception 反射或异步等待失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldPruneCompletedSchedulesBeforeCapacityCheck() throws Exception {
|
||||
InMemoryTriggerStore store = new InMemoryTriggerStore();
|
||||
ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(2);
|
||||
ExecutorService worker = Executors.newFixedThreadPool(2);
|
||||
TriggerScheduler scheduler =
|
||||
new TriggerScheduler(store, scheduledExecutor, worker, 1000L);
|
||||
CountDownLatch consumed = new CountDownLatch(1);
|
||||
scheduler.registerConsumer((current, executor) -> consumed.countDown());
|
||||
|
||||
Field field = TriggerScheduler.class.getDeclaredField("scheduledFutures");
|
||||
field.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, ScheduledFuture<?>> localFutures =
|
||||
(Map<String, ScheduledFuture<?>>) field.get(scheduler);
|
||||
ScheduledFuture<?> completed = scheduledExecutor.schedule(
|
||||
() -> {
|
||||
},
|
||||
0L,
|
||||
TimeUnit.MILLISECONDS);
|
||||
completed.get(1, TimeUnit.SECONDS);
|
||||
for (int index = 0; index < 1024; index++) {
|
||||
localFutures.put("completed-" + index, completed);
|
||||
}
|
||||
|
||||
try {
|
||||
scheduler.schedule(dueTrigger("after-completed-capacity"));
|
||||
Assert.assertTrue(
|
||||
"completed local schedules should not block new due triggers",
|
||||
consumed.await(2, TimeUnit.SECONDS));
|
||||
Assert.assertTrue(localFutures.size() < 1024);
|
||||
} finally {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证确认成功状态写入异常时仍归还调度容量许可。
|
||||
*
|
||||
* @throws Exception 等待异步执行被中断时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldReturnDispatchPermitWhenAcknowledgeFails() throws Exception {
|
||||
assertTerminalStoreFailureDoesNotLeakPermit(TerminalOperation.ACKNOWLEDGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证失败重投状态写入异常时仍归还调度容量许可。
|
||||
*
|
||||
* @throws Exception 等待异步执行被中断时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldReturnDispatchPermitWhenReleaseFails() throws Exception {
|
||||
assertTerminalStoreFailureDoesNotLeakPermit(TerminalOperation.RELEASE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证死信状态写入异常时仍归还调度容量许可。
|
||||
*
|
||||
* @throws Exception 等待异步执行被中断时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldReturnDispatchPermitWhenDeadLetterFails() throws Exception {
|
||||
assertTerminalStoreFailureDoesNotLeakPermit(TerminalOperation.DEAD_LETTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行终态仓储异常后的容量许可回归场景。
|
||||
*
|
||||
* @param operation 需要模拟失败的终态操作
|
||||
* @throws Exception 等待异步执行被中断时抛出
|
||||
*/
|
||||
private void assertTerminalStoreFailureDoesNotLeakPermit(
|
||||
TerminalOperation operation) throws Exception {
|
||||
FailingTerminalStore store = new FailingTerminalStore(operation);
|
||||
ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(2);
|
||||
ThreadPoolExecutor worker = new ThreadPoolExecutor(
|
||||
1,
|
||||
1,
|
||||
0L,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new SynchronousQueue<>());
|
||||
TriggerScheduler scheduler =
|
||||
new TriggerScheduler(store, scheduledExecutor, worker, 1000L);
|
||||
CountDownLatch secondConsumed = new CountDownLatch(1);
|
||||
scheduler.registerConsumer((current, executor) -> {
|
||||
if ("second".equals(current.getId())) {
|
||||
secondConsumed.countDown();
|
||||
return;
|
||||
}
|
||||
if (operation == TerminalOperation.DEAD_LETTER) {
|
||||
throw new NonRetryableTriggerException("expected dead-letter failure");
|
||||
}
|
||||
if (operation == TerminalOperation.RELEASE) {
|
||||
throw new IllegalStateException("expected release failure");
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
Trigger first = futureTrigger("first");
|
||||
store.save(first);
|
||||
Assert.assertTrue(scheduler.fire(first.getId()));
|
||||
Assert.assertTrue(store.terminalAttempted.await(2, TimeUnit.SECONDS));
|
||||
|
||||
store.fail = false;
|
||||
Trigger second = futureTrigger("second");
|
||||
store.save(second);
|
||||
boolean accepted = false;
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2);
|
||||
while (!accepted && System.nanoTime() < deadline) {
|
||||
accepted = scheduler.fire(second.getId());
|
||||
if (!accepted) {
|
||||
Thread.sleep(10L);
|
||||
}
|
||||
}
|
||||
Assert.assertTrue("dispatch permit should be returned", accepted);
|
||||
Assert.assertTrue(secondConsumed.await(2, TimeUnit.SECONDS));
|
||||
} finally {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建测试调度器。
|
||||
*
|
||||
* @param store 触发器仓储
|
||||
* @return 测试调度器
|
||||
*/
|
||||
private TriggerScheduler scheduler(TriggerStore store) {
|
||||
ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(2);
|
||||
ExecutorService worker = Executors.newFixedThreadPool(2);
|
||||
return new TriggerScheduler(store, scheduledExecutor, worker, 1000L);
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待指定本地调度索引清空。
|
||||
*
|
||||
* @param scheduler 调度器
|
||||
* @param fieldName 索引字段名
|
||||
* @throws Exception 反射读取失败或等待超时时抛出
|
||||
*/
|
||||
private void awaitLocalScheduleIndexEmpty(
|
||||
TriggerScheduler scheduler,
|
||||
String fieldName) throws Exception {
|
||||
Field field =
|
||||
TriggerScheduler.class.getDeclaredField(
|
||||
fieldName);
|
||||
field.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, ?> index =
|
||||
(Map<String, ?>) field.get(scheduler);
|
||||
long deadline =
|
||||
System.nanoTime()
|
||||
+ TimeUnit.SECONDS.toNanos(2L);
|
||||
while (!index.isEmpty()
|
||||
&& System.nanoTime() < deadline) {
|
||||
Thread.sleep(10L);
|
||||
}
|
||||
Assert.assertTrue(
|
||||
fieldName + " should be empty",
|
||||
index.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建远期触发器,避免定时任务干扰主动认领测试。
|
||||
*
|
||||
* @param id 触发器 ID
|
||||
* @return 测试触发器
|
||||
*/
|
||||
private Trigger futureTrigger(String id) {
|
||||
return nearFutureTrigger(
|
||||
id,
|
||||
TimeUnit.MINUTES.toMillis(5));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建指定延迟的测试触发器。
|
||||
*
|
||||
* @param id 触发器 ID
|
||||
* @param delayMillis 延迟毫秒数
|
||||
* @return 测试触发器
|
||||
*/
|
||||
private Trigger nearFutureTrigger(
|
||||
String id,
|
||||
long delayMillis) {
|
||||
Trigger trigger = new Trigger();
|
||||
trigger.setId(id);
|
||||
trigger.setTriggerAt(
|
||||
System.currentTimeMillis()
|
||||
+ delayMillis);
|
||||
return trigger;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建立即到期的测试触发器。
|
||||
*
|
||||
* @param id 触发器 ID
|
||||
* @return 测试触发器
|
||||
*/
|
||||
private Trigger dueTrigger(String id) {
|
||||
Trigger trigger = new Trigger();
|
||||
trigger.setId(id);
|
||||
trigger.setTriggerAt(System.currentTimeMillis());
|
||||
return trigger;
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待失败触发器重新进入待执行状态。
|
||||
*
|
||||
* @param store 触发器仓储
|
||||
* @param triggerId 触发器 ID
|
||||
* @throws InterruptedException 等待被中断时抛出
|
||||
*/
|
||||
private void awaitPending(InMemoryTriggerStore store, String triggerId) throws InterruptedException {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2);
|
||||
while (store.find(triggerId) == null && System.nanoTime() < deadline) {
|
||||
Thread.sleep(10L);
|
||||
}
|
||||
Assert.assertNotNull(store.find(triggerId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 需要模拟失败的触发器终态操作。
|
||||
*/
|
||||
private enum TerminalOperation {
|
||||
ACKNOWLEDGE,
|
||||
RELEASE,
|
||||
DEAD_LETTER
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定终态操作上抛出异常的进程内触发器仓储。
|
||||
*/
|
||||
private static final class FailingTerminalStore extends InMemoryTriggerStore {
|
||||
|
||||
private final TerminalOperation operation;
|
||||
private final CountDownLatch terminalAttempted = new CountDownLatch(1);
|
||||
private volatile boolean fail = true;
|
||||
|
||||
/**
|
||||
* 创建失败仓储。
|
||||
*
|
||||
* @param operation 需要失败的终态操作
|
||||
*/
|
||||
private FailingTerminalStore(TerminalOperation operation) {
|
||||
this.operation = operation;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void acknowledge(Trigger trigger) {
|
||||
failIfNeeded(TerminalOperation.ACKNOWLEDGE);
|
||||
super.acknowledge(trigger);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void release(Trigger trigger) {
|
||||
failIfNeeded(TerminalOperation.RELEASE);
|
||||
super.release(trigger);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void deadLetter(Trigger trigger, String reason) {
|
||||
failIfNeeded(TerminalOperation.DEAD_LETTER);
|
||||
super.deadLetter(trigger, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前测试操作上抛出预期异常。
|
||||
*
|
||||
* @param current 当前终态操作
|
||||
*/
|
||||
private void failIfNeeded(TerminalOperation current) {
|
||||
if (fail && operation == current) {
|
||||
terminalAttempted.countDown();
|
||||
throw new IllegalStateException("expected terminal store failure: " + current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟 claim 已转移给其他 owner 的触发器仓储。
|
||||
*/
|
||||
private static final class LostClaimStore
|
||||
extends InMemoryTriggerStore {
|
||||
|
||||
private final CountDownLatch finalizationRejected =
|
||||
new CountDownLatch(1);
|
||||
private final AtomicInteger deadLetterAttempts =
|
||||
new AtomicInteger();
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean renewClaim(
|
||||
Trigger trigger, long leaseMillis) {
|
||||
finalizationRejected.countDown();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void deadLetter(
|
||||
Trigger trigger, String reason) {
|
||||
deadLetterAttempts.incrementAndGet();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user