perf: 优化工作流执行引擎与循环调度

- 引入增量状态、定义快照和持久化触发器

- 收敛循环结果、模板条件和高 IO 执行开销

- 统一循环 1 至 300 次约束并补充并发回归测试
This commit is contained in:
2026-07-29 00:47:41 +08:00
parent a7e89cee3d
commit c72a167633
70 changed files with 11338 additions and 472 deletions

View File

@@ -20,16 +20,25 @@ import com.easyagents.flow.core.util.StringUtil;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
public class ChainDefinition implements Serializable { public class ChainDefinition implements Serializable {
private static final long serialVersionUID = -3183115191738959423L;
protected String id; protected String id;
protected String name; protected String name;
protected String description; protected String description;
protected List<Node> nodes; protected List<Node> nodes;
protected List<Edge> edges; protected List<Edge> edges;
/**
* 由节点和边派生的只读图索引,不参与序列化。
*/
private transient volatile GraphIndex graphIndex;
public ChainDefinition() { public ChainDefinition() {
} }
@@ -64,6 +73,7 @@ public class ChainDefinition implements Serializable {
public void setNodes(List<Node> nodes) { public void setNodes(List<Node> nodes) {
this.nodes = nodes; this.nodes = nodes;
invalidateGraphIndex();
} }
public List<Edge> getEdges() { public List<Edge> getEdges() {
@@ -72,27 +82,45 @@ public class ChainDefinition implements Serializable {
public void setEdges(List<Edge> edges) { public void setEdges(List<Edge> edges) {
this.edges = edges; this.edges = edges;
invalidateGraphIndex();
} }
/**
* 获取指定节点的全部出边。
*
* @param nodeId 节点 ID
* @return 保持定义顺序的出边副本
*/
public List<Edge> getOutwardEdge(String nodeId) { public List<Edge> getOutwardEdge(String nodeId) {
List<Edge> result = new ArrayList<>(); List<Edge> outwardEdges = graphIndex().outwardEdgesByNode.get(nodeId);
for (Edge edge : edges) { return outwardEdges == null ? Collections.emptyList() : new ArrayList<>(outwardEdges);
if (nodeId.equals(edge.getSource())) {
result.add(edge);
}
}
return result;
} }
/**
* 获取指定节点的全部入边。
*
* @param nodeId 节点 ID
* @return 保持定义顺序的入边副本
*/
public List<Edge> getInwardEdge(String nodeId) { public List<Edge> getInwardEdge(String nodeId) {
List<Edge> result = new ArrayList<>(); List<Edge> inwardEdges = graphIndex().inwardEdgesByNode.get(nodeId);
for (Edge edge : edges) { return inwardEdges == null ? Collections.emptyList() : new ArrayList<>(inwardEdges);
if (nodeId.equals(edge.getTarget())) {
result.add(edge);
} }
}
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) { public void addNode(Node node) {
@@ -105,31 +133,21 @@ public class ChainDefinition implements Serializable {
} }
nodes.add(node); nodes.add(node);
invalidateGraphIndex();
// 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);
// }
// }
// }
} }
/**
* 按 ID 获取节点。
*
* @param id 节点 ID
* @return 对应节点,不存在时返回 {@code null}
*/
public Node getNodeById(String id) { public Node getNodeById(String id) {
if (id == null || StringUtil.noText(id)) { if (id == null || StringUtil.noText(id)) {
return null; return null;
} }
return graphIndex().nodeById.get(id);
for (Node node : this.nodes) {
if (id.equals(node.getId())) {
return node;
}
}
return null;
} }
@@ -138,49 +156,33 @@ public class ChainDefinition implements Serializable {
this.edges = new ArrayList<>(); this.edges = new ArrayList<>();
} }
this.edges.add(edge); this.edges.add(edge);
invalidateGraphIndex();
// 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;
// }
// }
} }
/**
* 按 ID 获取边。
*
* @param edgeId 边 ID
* @return 对应边,不存在时返回 {@code null}
*/
public Edge getEdgeById(String edgeId) { public Edge getEdgeById(String edgeId) {
for (Edge edge : this.edges) { if (StringUtil.noText(edgeId)) {
if (edgeId.equals(edge.getId())) {
return edge;
}
}
return null; return null;
} }
return graphIndex().edgeById.get(edgeId);
}
/**
* 获取没有入边的开始节点。
*
* @return 保持定义顺序的开始节点副本
*/
public List<Node> getStartNodes() { public List<Node> getStartNodes() {
if (nodes == null || nodes.isEmpty()) { if (nodes == null || nodes.isEmpty()) {
return null; return null;
} }
return new ArrayList<>(graphIndex().startNodes);
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;
} }
@@ -198,6 +200,210 @@ public class ChainDefinition implements Serializable {
return parameters; 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 @Override
public String toString() { public String toString() {

View File

@@ -37,8 +37,14 @@ import java.util.stream.Collectors;
public class ChainState implements Serializable { public class ChainState implements Serializable {
private static final long serialVersionUID = -7958235553581638052L;
private String instanceId; private String instanceId;
private String parentInstanceId; private String parentInstanceId;
/**
* 节点审计应归属的顶级执行实例 ID。
*/
private String auditInstanceId;
private String chainDefinitionId; private String chainDefinitionId;
private ConcurrentHashMap<String, Object> memory = new ConcurrentHashMap<>(); private ConcurrentHashMap<String, Object> memory = new ConcurrentHashMap<>();
@@ -59,9 +65,18 @@ public class ChainState implements Serializable {
private String message; private String message;
private ExceptionSummary error; private ExceptionSummary error;
private long version; private long version;
/**
* 工作流实例首次启动时间,用于跨线程和跨进程执行时长保护。
*/
private long startedAt;
/**
* 已进入业务执行的节点次数,用于全局执行预算。
*/
private long childExecutionCount;
public ChainState() { public ChainState() {
this.instanceId = UUID.randomUUID().toString(); this.instanceId = UUID.randomUUID().toString();
this.auditInstanceId = this.instanceId;
this.status = ChainStatus.READY; this.status = ChainStatus.READY;
this.computeCost = 0; this.computeCost = 0;
} }
@@ -71,7 +86,15 @@ public class ChainState implements Serializable {
} }
public void setInstanceId(String instanceId) { public void setInstanceId(String instanceId) {
String previousInstanceId =
this.instanceId;
this.instanceId = instanceId; this.instanceId = instanceId;
if (auditInstanceId == null
|| Objects.equals(
auditInstanceId,
previousInstanceId)) {
auditInstanceId = instanceId;
}
} }
public String getParentInstanceId() { public String getParentInstanceId() {
@@ -80,6 +103,30 @@ public class ChainState implements Serializable {
public void setParentInstanceId(String parentInstanceId) { public void setParentInstanceId(String parentInstanceId) {
this.parentInstanceId = 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() { public String getChainDefinitionId() {
@@ -127,8 +174,10 @@ public class ChainState implements Serializable {
if (triggerEdgeIds == null) { if (triggerEdgeIds == null) {
triggerEdgeIds = new ArrayList<>(); triggerEdgeIds = new ArrayList<>();
} }
if (!triggerEdgeIds.contains(edgeId)) {
triggerEdgeIds.add(edgeId); triggerEdgeIds.add(edgeId);
} }
}
public List<String> getTriggerNodeIds() { public List<String> getTriggerNodeIds() {
return triggerNodeIds; return triggerNodeIds;
@@ -142,29 +191,41 @@ public class ChainState implements Serializable {
if (triggerNodeIds == null) { if (triggerNodeIds == null) {
triggerNodeIds = new ArrayList<>(); triggerNodeIds = new ArrayList<>();
} }
if (!triggerNodeIds.contains(nodeId)) {
triggerNodeIds.add(nodeId); triggerNodeIds.add(nodeId);
} }
}
public List<String> getUncheckedEdgeIds() { public List<String> getUncheckedEdgeIds() {
return uncheckedEdgeIds; return uncheckedEdgeIds;
} }
public void setUncheckedEdgeIds(List<String> 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) { if (uncheckedEdgeIds == null) {
uncheckedEdgeIds = new ArrayList<>(); uncheckedEdgeIds = new ArrayList<>();
} }
if (uncheckedEdgeIds.contains(edgeId)) {
return false;
}
uncheckedEdgeIds.add(edgeId); uncheckedEdgeIds.add(edgeId);
return true;
} }
public boolean removeUncheckedEdgeId(String edgeId) { public boolean removeUncheckedEdgeId(String edgeId) {
if (uncheckedEdgeIds == null) { if (uncheckedEdgeIds == null) {
return false; return false;
} }
return uncheckedEdgeIds.remove(edgeId); boolean removed = false;
while (uncheckedEdgeIds.remove(edgeId)) {
removed = true;
}
return removed;
} }
public List<String> getUncheckedNodeIds() { public List<String> getUncheckedNodeIds() {
@@ -172,21 +233,31 @@ public class ChainState implements Serializable {
} }
public void setUncheckedNodeIds(List<String> uncheckedNodeIds) { 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) { if (uncheckedNodeIds == null) {
uncheckedNodeIds = new ArrayList<>(); uncheckedNodeIds = new ArrayList<>();
} }
if (uncheckedNodeIds.contains(nodeId)) {
return false;
}
uncheckedNodeIds.add(nodeId); uncheckedNodeIds.add(nodeId);
return true;
} }
public boolean removeUncheckedNodeId(String nodeId) { public boolean removeUncheckedNodeId(String nodeId) {
if (uncheckedNodeIds == null) { if (uncheckedNodeIds == null) {
return false; return false;
} }
return uncheckedNodeIds.remove(nodeId); boolean removed = false;
while (uncheckedNodeIds.remove(nodeId)) {
removed = true;
}
return removed;
} }
public Long getComputeCost() { public Long getComputeCost() {
@@ -281,6 +352,22 @@ public class ChainState implements Serializable {
this.version = version; 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) { public static ChainState fromJSON(String jsonString) {
ParserConfig config = new ParserConfig(); ParserConfig config = new ParserConfig();
config.putDeserializer(ChainState.class, new ChainDeserializer()); config.putDeserializer(ChainState.class, new ChainDeserializer());
@@ -305,6 +392,8 @@ public class ChainState implements Serializable {
this.status = ChainStatus.READY; this.status = ChainStatus.READY;
this.message = null; this.message = null;
this.error = null; this.error = null;
this.startedAt = 0L;
this.childExecutionCount = 0L;
} }
@@ -340,10 +429,46 @@ public class ChainState implements Serializable {
public Object resolveValue(String path) { 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); Object result = MapUtil.getByPath(getMemory(), path);
if (result == null) result = MapUtil.getByPath(getEnvironment(), path); if (result == null) result = MapUtil.getByPath(getEnvironment(), path);
// if (result == null) result = MapUtil.getByPath(getTriggerVariables(), 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) { public Map<String, Object> resolveParameters(Node node) {
@@ -381,7 +506,28 @@ public class ChainState implements Serializable {
* @return 模板渲染上下文列表 * @return 模板渲染上下文列表
*/ */
public List<Map<String, Object>> buildTemplateRootMaps(Map<String, Object> formatArgs) { 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) { 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()) { if (parameters == null || parameters.isEmpty()) {
return Collections.emptyMap(); return Collections.emptyMap();
} }
Map<String, Object> variables = new LinkedHashMap<>(); Map<String, Object> variables = new LinkedHashMap<>();
List<Parameter> suspendParameters = null; List<Parameter> suspendParameters = null;
List<Map<String, Object>> templateRootMaps = null;
for (Parameter parameter : parameters) { for (Parameter parameter : parameters) {
RefType refType = parameter.getRefType(); RefType refType = parameter.getRefType();
Object value = null; Object value = null;
if (refType == RefType.FIXED) { if (refType == RefType.FIXED) {
if (templateRootMaps == null) {
templateRootMaps =
preserveDirectReferences
? buildLazyTemplateRootMaps(
formatArgs)
: buildTemplateRootMaps(
formatArgs);
}
value = TextTemplate.of(parameter.getValue()) value = TextTemplate.of(parameter.getValue())
.formatToString(buildTemplateRootMaps(formatArgs)); .formatToString(templateRootMaps);
} else if (refType == RefType.REF) { } else if (refType == RefType.REF) {
value = this.resolveValue(parameter.getRef()); value = this.resolveValue(
parameter.getRef(),
preserveDirectReferences);
} }
// 单节点执行时,参数只会传入 name 内容。 // 单节点执行时,参数只会传入 name 内容。
if (value == null) { if (value == null) {
value = this.resolveValue(parameter.getName()); value = this.resolveValue(
parameter.getName(),
preserveDirectReferences);
} }
if (value == null && parameter.getDefaultValue() != null) { if (value == null && parameter.getDefaultValue() != null) {
@@ -475,6 +674,166 @@ public class ChainState implements Serializable {
return variables; 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 { public static class ChainSerializer implements ObjectSerializer {
@Override @Override
@@ -513,6 +872,8 @@ public class ChainState implements Serializable {
", message='" + message + '\'' + ", message='" + message + '\'' +
", error=" + error + ", error=" + error +
", version=" + version + ", version=" + version +
", startedAt=" + startedAt +
", childExecutionCount=" + childExecutionCount +
'}'; '}';
} }
} }

View File

@@ -16,7 +16,14 @@
package com.easyagents.flow.core.chain; 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 id;
private String source; private String source;
private String target; private String target;

View File

@@ -16,10 +16,22 @@
package com.easyagents.flow.core.chain; package com.easyagents.flow.core.chain;
import java.io.Serializable;
import java.util.Map; 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); boolean check(Chain chain, Edge edge, Map<String, Object> executeResult);
} }

View File

@@ -20,6 +20,7 @@ import java.io.Serializable;
import java.io.StringWriter; import java.io.StringWriter;
public class ExceptionSummary implements Serializable { public class ExceptionSummary implements Serializable {
private static final long serialVersionUID = 1L;
private String exceptionClass; private String exceptionClass;
private String message; private String message;
@@ -134,4 +135,3 @@ public class ExceptionSummary implements Serializable {
this.timestamp = timestamp; this.timestamp = timestamp;
} }
} }

View File

@@ -25,7 +25,12 @@ import java.util.List;
import java.util.Map; import java.util.Map;
public abstract class Node implements Serializable { public abstract class Node implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger log = org.slf4j.LoggerFactory.getLogger(Node.class); 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 id;
protected String parentId; protected String parentId;
@@ -42,7 +47,7 @@ public abstract class Node implements Serializable {
protected boolean loopEnable = false; // 是否启用循环执行 protected boolean loopEnable = false; // 是否启用循环执行
protected long loopIntervalMs = 3000; // 循环间隔时间(毫秒) protected long loopIntervalMs = 3000; // 循环间隔时间(毫秒)
protected NodeCondition loopBreakCondition; // 跳出循环的条件 protected NodeCondition loopBreakCondition; // 跳出循环的条件
protected int maxLoopCount = 0; // 0 表示不限制循环次数 protected int maxLoopCount = MIN_LOOP_COUNT; // 循环总执行次数,取值范围 1300
protected boolean retryEnable = false; protected boolean retryEnable = false;
protected boolean resetRetryCountAfterNormal = false; protected boolean resetRetryCountAfterNormal = false;
@@ -158,7 +163,22 @@ public abstract class Node implements Serializable {
return maxLoopCount; return maxLoopCount;
} }
/**
* 设置节点循环的总执行次数。
*
* @param maxLoopCount 总执行次数,范围为 1300
* @throws IllegalArgumentException 循环次数超出允许范围
*/
public void setMaxLoopCount(int maxLoopCount) { 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; this.maxLoopCount = maxLoopCount;
} }
@@ -237,7 +257,12 @@ public abstract class Node implements Serializable {
protected long doCalculateComputeCost(String expr, Chain chain, Map<String, Object> result) { 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().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); Map<String, Object> newMap = new HashMap<>(result);
newMap.putAll(parameterValues); newMap.putAll(parameterValues);
return JsConditionUtil.evalLong(expr, chain, newMap); return JsConditionUtil.evalLong(expr, chain, newMap);

View File

@@ -16,10 +16,22 @@
package com.easyagents.flow.core.chain; package com.easyagents.flow.core.chain;
import java.io.Serializable;
import java.util.Map; 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); boolean check(Chain chain, NodeState context, Map<String, Object> executeResult);
} }

View File

@@ -20,10 +20,11 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
public class NodeState implements Serializable { public class NodeState implements Serializable {
private static final long serialVersionUID = -6727481826462129573L;
private String nodeId; private String nodeId;
private String chainInstanceId; private String chainInstanceId;
@@ -39,6 +40,11 @@ public class NodeState implements Serializable {
private AtomicInteger executeCount = new AtomicInteger(0); private AtomicInteger executeCount = new AtomicInteger(0);
private List<String> executeEdgeIds = new ArrayList<>(); private List<String> executeEdgeIds = new ArrayList<>();
/**
* 当前节点生命周期对应的稳定业务尝试键。
*/
private String executionAttemptKey;
ExceptionSummary error; ExceptionSummary error;
private long version; private long version;
@@ -135,6 +141,26 @@ public class NodeState implements Serializable {
this.executeEdgeIds = executeEdgeIds; this.executeEdgeIds = executeEdgeIds;
} }
/**
* 获取当前节点生命周期的稳定业务尝试键。
*
* @return 稳定业务尝试键
*/
public String getExecutionAttemptKey() {
return executionAttemptKey;
}
/**
* 设置当前节点生命周期的稳定业务尝试键。
*
* @param executionAttemptKey 稳定业务尝试键
*/
public void setExecutionAttemptKey(
String executionAttemptKey) {
this.executionAttemptKey =
executionAttemptKey;
}
public ExceptionSummary getError() { public ExceptionSummary getError() {
return error; return error;
} }
@@ -158,10 +184,16 @@ public class NodeState implements Serializable {
return true; return true;
} }
List<String> shouldBeTriggerIds = inwardEdges.stream().map(Edge::getId).collect(Collectors.toList()); if (triggerEdgeIds.size() < inwardEdges.size()) {
List<String> triggerEdgeIds = this.triggerEdgeIds; return false;
return triggerEdgeIds.size() >= shouldBeTriggerIds.size() }
&& shouldBeTriggerIds.parallelStream().allMatch(triggerEdgeIds::contains); 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) { public void recordTrigger(String fromEdgeId) {
@@ -169,14 +201,17 @@ public class NodeState implements Serializable {
if (fromEdgeId == null) { if (fromEdgeId == null) {
fromEdgeId = "none"; fromEdgeId = "none";
} }
if (!triggerEdgeIds.contains(fromEdgeId)) {
triggerEdgeIds.add(fromEdgeId); triggerEdgeIds.add(fromEdgeId);
} }
}
public void recordExecute(String fromEdgeId) { public void recordExecute(String fromEdgeId) {
executeCount.incrementAndGet(); executeCount.incrementAndGet();
if (fromEdgeId == null) { if (fromEdgeId == null) {
fromEdgeId = "none"; fromEdgeId = "none";
} }
executeEdgeIds.clear();
executeEdgeIds.add(fromEdgeId); executeEdgeIds.add(fromEdgeId);
} }

View File

@@ -16,6 +16,18 @@
package com.easyagents.flow.core.chain; 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); NodeValidResult validate(Node node);
} }

View File

@@ -23,6 +23,7 @@ import java.util.Collection;
import java.util.List; import java.util.List;
public class Parameter implements Serializable, Cloneable { public class Parameter implements Serializable, Cloneable {
private static final long serialVersionUID = 1L;
protected String id; protected String id;
protected String name; protected String name;
protected String description; protected String description;

View File

@@ -18,40 +18,133 @@ package com.easyagents.flow.core.chain.event;
import com.easyagents.flow.core.chain.Chain; import com.easyagents.flow.core.chain.Chain;
import com.easyagents.flow.core.chain.Node; import com.easyagents.flow.core.chain.Node;
import com.easyagents.flow.core.chain.NodeStatus;
import java.util.Map; import java.util.Map;
/**
* 节点结束执行事件。
*/
public class NodeEndEvent extends BaseEvent { public class NodeEndEvent extends BaseEvent {
private final Node node; private final Node node;
private final Map<String, Object> result; private final Map<String, Object> result;
private final Throwable error; 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) { 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); super(chain);
this.node = node; this.node = node;
this.result = result; this.result = result;
this.error = error; this.error = error;
this.status = status;
this.executionAttemptKey = executionAttemptKey;
} }
/**
* 获取当前节点。
*
* @return 当前节点
*/
public Node getNode() { public Node getNode() {
return node; return node;
} }
/**
* 获取节点输出。
*
* @return 节点输出
*/
public Map<String, Object> getResult() { public Map<String, Object> getResult() {
return result; return result;
} }
/**
* 获取节点异常。
*
* @return 节点异常;成功时为 {@code null}
*/
public Throwable getError() { public Throwable getError() {
return error; return error;
} }
/**
* 获取事件创建时捕获的节点终态。
*
* @return 节点终态;旧调用方未提供时为 {@code null}
*/
public NodeStatus getStatus() {
return status;
}
/**
* 获取事件创建时捕获的业务尝试键。
*
* @return 业务尝试键;旧调用方未提供时为 {@code null}
*/
public String getExecutionAttemptKey() {
return executionAttemptKey;
}
@Override @Override
public String toString() { public String toString() {
return "NodeEndEvent{" + return "NodeEndEvent{" +
"node=" + node + "node=" + node +
", result=" + result + ", result=" + result +
", error=" + error + ", error=" + error +
", status=" + status +
", executionAttemptKey='" + executionAttemptKey + '\'' +
", chain=" + chain + ", chain=" + chain +
'}'; '}';
} }

View File

@@ -18,25 +18,123 @@ package com.easyagents.flow.core.chain.event;
import com.easyagents.flow.core.chain.Chain; import com.easyagents.flow.core.chain.Chain;
import com.easyagents.flow.core.chain.Node; import com.easyagents.flow.core.chain.Node;
import com.easyagents.flow.core.chain.NodeStatus;
/**
* 节点开始执行事件。
*/
public class NodeStartEvent extends BaseEvent { public class NodeStartEvent extends BaseEvent {
private final Node node; 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) { public NodeStartEvent(Chain chain, Node node) {
super(chain); this(chain, node, null, null,
this.node = node; 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() { public Node getNode() {
return node; 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 @Override
public String toString() { public String toString() {
return "NodeStartEvent{" + return "NodeStartEvent{" +
"node=" + node + "node=" + node +
", executionAttemptKey='" + executionAttemptKey + '\'' +
", status=" + status +
", auditInstanceId='" + auditInstanceId + '\'' +
", chain=" + chain + ", chain=" + chain +
'}'; '}';
} }

View File

@@ -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);
}

View File

@@ -25,6 +25,24 @@ public interface ChainLock extends AutoCloseable {
*/ */
boolean isAcquired(); boolean isAcquired();
/**
* 锁是否仍由当前 owner 持有。
*
* @return 锁仍有效时为 true
*/
default boolean isValid() {
return isAcquired();
}
/**
* 获取本次锁持有期对应的 fencing token。
*
* @return 分布式仓储生成的单实例单调递增 token本地锁返回 {@code 0}
*/
default long getFencingToken() {
return 0L;
}
/** /**
* 释放锁(幂等) * 释放锁(幂等)
*/ */

View File

@@ -31,8 +31,12 @@ public enum ChainStateField {
ENVIRONMENT, ENVIRONMENT,
CHILD_STATE_IDS, CHILD_STATE_IDS,
PARENT_INSTANCE_ID, PARENT_INSTANCE_ID,
AUDIT_INSTANCE_ID,
TRIGGER_NODE_IDS, TRIGGER_NODE_IDS,
TRIGGER_EDGE_IDS, TRIGGER_EDGE_IDS,
UNCHECKED_EDGE_IDS, UNCHECKED_EDGE_IDS,
UNCHECKED_NODE_IDS; UNCHECKED_NODE_IDS,
STARTED_AT,
CHILD_EXECUTION_COUNT,
VERSION;
} }

View File

@@ -24,8 +24,70 @@ public interface ChainStateRepository {
ChainState load(String instanceId); 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); 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 的分布式锁 * 获取指定 instanceId 的分布式锁
* *

View File

@@ -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);
}
}

View File

@@ -16,24 +16,41 @@
package com.easyagents.flow.core.chain.repository; package com.easyagents.flow.core.chain.repository;
import com.easyagents.flow.core.chain.ChainState; import com.easyagents.flow.core.chain.ChainState;
import com.easyagents.flow.core.util.MapUtil;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/**
* 进程内工作流状态仓储。
*/
public class InMemoryChainStateRepository implements ChainStateRepository { public class InMemoryChainStateRepository implements ChainStateRepository {
private static final Map<String, ChainState> chainStateMap = new ConcurrentHashMap<>(); private static final Map<String, ChainState> chainStateMap = new ConcurrentHashMap<>();
/**
* {@inheritDoc}
*/
@Override @Override
public ChainState load(String instanceId) { 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(); ChainState state = new ChainState();
state.setInstanceId(instanceId); state.setInstanceId(instanceId);
return state; return state;
}); });
} }
/**
* {@inheritDoc}
*/
@Override @Override
public boolean tryUpdate(ChainState chainState, EnumSet<ChainStateField> fields) { public boolean tryUpdate(ChainState chainState, EnumSet<ChainStateField> fields) {
chainStateMap.put(chainState.getInstanceId(), chainState); chainStateMap.put(chainState.getInstanceId(), chainState);

View File

@@ -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;
}
}

View File

@@ -16,20 +16,33 @@
package com.easyagents.flow.core.chain.repository; package com.easyagents.flow.core.chain.repository;
import com.easyagents.flow.core.chain.NodeState; import com.easyagents.flow.core.chain.NodeState;
import com.easyagents.flow.core.util.MapUtil;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/**
* 进程内节点状态仓储。
*/
public class InMemoryNodeStateRepository implements NodeStateRepository { public class InMemoryNodeStateRepository implements NodeStateRepository {
private static final Map<String, NodeState> chainStateMap = new ConcurrentHashMap<>(); private static final Map<String, NodeState> chainStateMap = new ConcurrentHashMap<>();
/**
* {@inheritDoc}
*/
@Override @Override
public NodeState load(String instanceId, String nodeId) { 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 nodeState = new NodeState();
nodeState.setChainInstanceId(instanceId); nodeState.setChainInstanceId(instanceId);
nodeState.setNodeId(nodeId); nodeState.setNodeId(nodeId);
@@ -37,9 +50,23 @@ public class InMemoryNodeStateRepository implements NodeStateRepository {
}); });
} }
/**
* {@inheritDoc}
*/
@Override @Override
public boolean tryUpdate(NodeState newState, EnumSet<NodeStateField> fields, long version) { 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; return true;
} }
/**
* 构建进程内节点状态键。
*
* @param instanceId 工作流实例 ID
* @param nodeId 节点 ID
* @return 节点状态键
*/
private String key(String instanceId, String nodeId) {
return instanceId + "." + nodeId;
}
} }

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -27,5 +27,7 @@ public enum NodeStateField {
SUSPEND_NODE_IDS, SUSPEND_NODE_IDS,
SUSPEND_FOR_PARAMETERS, SUSPEND_FOR_PARAMETERS,
EXECUTE_RESULT, 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
} }

View File

@@ -21,7 +21,123 @@ import java.util.EnumSet;
public interface NodeStateRepository { public interface NodeStateRepository {
/**
* 加载已存在的节点状态。
*
* @param instanceId 工作流实例 ID
* @param nodeId 节点 ID
* @return 节点状态;纯读取实现可在状态缺失时返回 {@code null},兼容实现可惰性创建
*/
NodeState load(String instanceId, String nodeId); 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); 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);
}
} }

View File

@@ -25,6 +25,7 @@ import com.easyagents.flow.core.chain.repository.*;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
@@ -39,24 +40,61 @@ import java.util.concurrent.*;
public class ChainExecutor { public class ChainExecutor {
private static final Logger log = LoggerFactory.getLogger(ChainExecutor.class); 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 ChainDefinitionRepository definitionRepository;
private final ChainStateRepository chainStateRepository; private final ChainStateRepository chainStateRepository;
private final NodeStateRepository nodeStateRepository; private final NodeStateRepository nodeStateRepository;
private final LoopResultRepository loopResultRepository;
private final ChainDefinitionSnapshotRepository definitionSnapshotRepository;
private final TriggerScheduler triggerScheduler; private final TriggerScheduler triggerScheduler;
private final ExecutionBudget executionBudget;
private final EventManager eventManager = new EventManager(); private final EventManager eventManager = new EventManager();
/** 等待同步执行结果的任务,按工作流实例 ID 进行常量时间路由。 */ /** 等待同步执行结果的任务,按工作流实例 ID 进行常量时间路由。 */
private final ConcurrentMap<String, CompletableFuture<Map<String, Object>>> pendingExecutions = private final ConcurrentMap<String, CompletableFuture<Map<String, Object>>> pendingExecutions =
new ConcurrentHashMap<>(); 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 public ChainExecutor(ChainDefinitionRepository definitionRepository
, ChainStateRepository chainStateRepository , ChainStateRepository chainStateRepository
, NodeStateRepository nodeStateRepository , NodeStateRepository nodeStateRepository
) { ) {
this.definitionRepository = definitionRepository; this(definitionRepository,
this.chainStateRepository = chainStateRepository; chainStateRepository,
this.nodeStateRepository = nodeStateRepository; nodeStateRepository,
this.triggerScheduler = ChainRuntime.triggerScheduler(); new InMemoryLoopResultRepository(),
registerRuntimeCallbacks(); new InMemoryChainDefinitionSnapshotRepository(),
ChainRuntime.triggerScheduler(),
ExecutionBudget.defaults());
} }
@@ -64,10 +102,92 @@ public class ChainExecutor {
, ChainStateRepository chainStateRepository , ChainStateRepository chainStateRepository
, NodeStateRepository nodeStateRepository , NodeStateRepository nodeStateRepository
, TriggerScheduler triggerScheduler) { , 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.definitionRepository = definitionRepository;
this.chainStateRepository = chainStateRepository; this.chainStateRepository = chainStateRepository;
this.nodeStateRepository = nodeStateRepository; this.nodeStateRepository = nodeStateRepository;
this.loopResultRepository = loopResultRepository == null
? new InMemoryLoopResultRepository()
: loopResultRepository;
this.definitionSnapshotRepository = definitionSnapshotRepository == null
? new InMemoryChainDefinitionSnapshotRepository()
: definitionSnapshotRepository;
this.triggerScheduler = triggerScheduler; this.triggerScheduler = triggerScheduler;
this.executionBudget = executionBudget == null ? ExecutionBudget.defaults() : executionBudget;
registerRuntimeCallbacks(); registerRuntimeCallbacks();
} }
@@ -80,33 +200,47 @@ public class ChainExecutor {
public Map<String, Object> execute(String definitionId, Map<String, Object> variables, long timeout, TimeUnit unit) { public Map<String, Object> execute(String definitionId, Map<String, Object> variables, long timeout, TimeUnit unit) {
Chain chain = createChain(definitionId); Chain chain = createChain(definitionId);
String stateInstanceId = chain.getStateInstanceId(); 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 { try {
chain.start(variables); chain.start(variables);
Map<String, Object> result = future.get(timeout, unit); Map<String, Object> result = awaitPersistentOutcome(
stateInstanceId, timeout, unit, null);
clearDefaultStates(result); clearDefaultStates(result);
return result; return result;
} catch (TimeoutException e) { } catch (TimeoutException e) {
future.cancel(true); cancel(stateInstanceId, "Execution timed out");
throw new RuntimeException("Execution timed out", e); throw new RuntimeException("Execution timed out", e);
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
future.cancel(true); cancel(stateInstanceId, "Execution interrupted");
throw new RuntimeException("Execution interrupted", e); throw new RuntimeException("Execution interrupted", e);
} catch (Throwable e) { } catch (Throwable e) {
future.cancel(true); throw new RuntimeException("Execution failed", e);
throw new RuntimeException("Execution failed", e.getCause());
} finally { } 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.addEventListener(ChainStatusChangeEvent.class, this::completePendingExecution);
eventManager.addChainErrorListener(this::failPendingExecution); eventManager.addChainErrorListener(this::failPendingExecution);
triggerScheduler.registerConsumer(this::accept); 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(); String stateInstanceId = chain.getStateInstanceId();
CompletableFuture<Map<String, Object>> future = pendingExecutions.get(stateInstanceId); CompletableFuture<Map<String, Object>> future = pendingExecutions.get(stateInstanceId);
if (future == null) {
return;
}
try { try {
if (future != null) {
ChainState state = chainStateRepository.load(stateInstanceId); ChainState state = chainStateRepository.load(stateInstanceId);
if (state == null) { if (state == null) {
throw new ChainException("Chain state not found: " + stateInstanceId); 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()); future.complete(execResult != null ? execResult : Collections.emptyMap());
}
} catch (Exception error) { } catch (Exception error) {
if (future != null) {
future.completeExceptionally(error); 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 发生异常的工作流实例 * @param chain 发生异常的工作流实例
*/ */
private void failPendingExecution(Throwable error, Chain 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) { if (future != null) {
future.completeExceptionally(error); 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) { public String executeAsync(String definitionId, Map<String, Object> variables) {
Chain chain = createChain(definitionId); Chain chain = createChain(definitionId);
try {
chain.start(variables); chain.start(variables);
return chain.getStateInstanceId(); 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) { public Map<String, Object> executeNode(String definitionId, String nodeId, Map<String, Object> variables) {
ChainDefinition chainDefinitionById = definitionRepository.getChainDefinitionById(definitionId); ChainDefinition chainDefinitionById = definitionRepository.getChainDefinitionById(definitionId);
Node node = chainDefinitionById.getNodeById(nodeId); Node node = chainDefinitionById.getNodeById(nodeId);
Chain temp = createChain(definitionId); Chain temp = createChain(chainDefinitionById);
try {
temp.initializeState();
if (variables != null && !variables.isEmpty()) { if (variables != null && !variables.isEmpty()) {
temp.updateStateSafely(s -> { temp.updateStateSafely(s -> {
s.getMemory().putAll(variables); s.getMemory().putAll(variables);
@@ -201,6 +833,10 @@ public class ChainExecutor {
}); });
} }
return node.execute(temp); return node.execute(temp);
} finally {
activeDefinitions.remove(temp.getStateInstanceId());
definitionSnapshotRepository.remove(temp.getStateInstanceId());
}
} }
@@ -229,17 +865,12 @@ public class ChainExecutor {
return; return;
} }
ChainDefinition definition = definitionRepository.getChainDefinitionById(state.getChainDefinitionId()); ChainDefinition definition = getDefinitionForInstance(state);
if (definition == null) { if (definition == null) {
return; return;
} }
Chain chain = new Chain(definition, state.getInstanceId()); Chain chain = configureChain(definition, state.getInstanceId());
chain.setTriggerScheduler(triggerScheduler);
chain.setChainStateRepository(chainStateRepository);
chain.setNodeStateRepository(nodeStateRepository);
chain.setEventManager(eventManager);
chain.resume(variables); chain.resume(variables);
} }
@@ -249,44 +880,132 @@ public class ChainExecutor {
if (definition == null) { if (definition == null) {
throw new RuntimeException("Chain definition not found"); throw new RuntimeException("Chain definition not found");
} }
return createChain(definition);
}
/**
* 使用已加载的定义创建工作流实例,避免同一次调用重复读取定义。
*
* @param definition 已加载的工作流定义
* @return 已完成运行时依赖配置的工作流实例
*/
private Chain createChain(ChainDefinition definition) {
String stateInstanceId = UUID.randomUUID().toString(); 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 chain = new Chain(definition, stateInstanceId);
chain.setTriggerScheduler(triggerScheduler); chain.setTriggerScheduler(triggerScheduler);
chain.setChainStateRepository(chainStateRepository); chain.setChainStateRepository(chainStateRepository);
chain.setNodeStateRepository(nodeStateRepository); chain.setNodeStateRepository(nodeStateRepository);
chain.setLoopResultRepository(loopResultRepository);
chain.setEventManager(eventManager); 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; 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) { private void accept(Trigger trigger, ExecutorService worker) {
ChainState state = chainStateRepository.load(trigger.getStateInstanceId()); ChainState state = chainStateRepository.load(trigger.getStateInstanceId());
if (state == null) { if (state == null) {
throw new ChainException("Chain state not found"); // 状态已过期或被清理时,该触发器已经失去业务目标,直接确认避免无限热重放。
return;
} }
ChainDefinition definition = definitionRepository.getChainDefinitionById(state.getChainDefinitionId()); ChainDefinition definition = getDefinitionForInstance(state);
if (definition == null) { 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 chain = configureChain(
chain.setTriggerScheduler(triggerScheduler); definition, trigger.getStateInstanceId(), state);
chain.setChainStateRepository(chainStateRepository);
chain.setNodeStateRepository(nodeStateRepository);
chain.setEventManager(eventManager);
String nodeId = trigger.getNodeId(); String nodeId = trigger.getNodeId();
if (nodeId == null) { 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); Node node = definition.getNodeById(nodeId);
if (node == null) { 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); chain.executeNode(node, trigger);
@@ -345,6 +1064,16 @@ public class ChainExecutor {
return triggerScheduler; return triggerScheduler;
} }
/**
* 在查询/API 边界透明还原循环结果引用。
*
* @param value 可能包含引用的值
* @return 业务可见值
*/
public Object resolveResultReferences(Object value) {
return loopResultRepository.resolveReferences(value);
}
public EventManager getEventManager() { public EventManager getEventManager() {
return eventManager; return eventManager;
} }

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -17,13 +17,16 @@ package com.easyagents.flow.core.chain.runtime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
public class InMemoryTriggerStore implements TriggerStore { public class InMemoryTriggerStore implements TriggerStore {
private final ConcurrentHashMap<String, Trigger> store = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, Trigger> store = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, AtomicLong> fencingTokens = new ConcurrentHashMap<>();
@Override @Override
public Trigger save(Trigger trigger) { public Trigger save(Trigger trigger) {
@@ -34,6 +37,18 @@ public class InMemoryTriggerStore implements TriggerStore {
return trigger; 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 @Override
public boolean remove(String triggerId) { public boolean remove(String triggerId) {
return store.remove(triggerId) != null; return store.remove(triggerId) != null;
@@ -46,12 +61,35 @@ public class InMemoryTriggerStore implements TriggerStore {
@Override @Override
public List<Trigger> findDue(long uptoTimestamp) { 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 @Override
public List<Trigger> findAllPending() { public List<Trigger> findAllPending() {
return new ArrayList<>(store.values()); 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;
}
}

View File

@@ -0,0 +1,16 @@
package com.easyagents.flow.core.chain.runtime;
/**
* 表示触发器内容已经无法继续执行,应进入死信而非无限重放。
*/
public class NonRetryableTriggerException extends RuntimeException {
/**
* 创建不可重试触发器异常。
*
* @param message 异常说明
*/
public NonRetryableTriggerException(String message) {
super(message);
}
}

View File

@@ -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);
}
}

View File

@@ -16,14 +16,65 @@
package com.easyagents.flow.core.chain.runtime; package com.easyagents.flow.core.chain.runtime;
import java.io.Serializable; import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;
public class Trigger implements Serializable { public class Trigger implements Serializable {
private static final long serialVersionUID = 3165037658498721088L;
private String id; private String id;
private String stateInstanceId; private String stateInstanceId;
private String edgeId; private String edgeId;
private String nodeId; // 可以为 null代表触发整个 chain private String nodeId; // 可以为 null代表触发整个 chain
private TriggerType type; private TriggerType type;
private long triggerAt; // epoch ms 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() { public Trigger() {
} }
@@ -77,6 +128,260 @@ public class Trigger implements Serializable {
this.triggerAt = triggerAt; 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 @Override
public String toString() { public String toString() {
return "Trigger{" + return "Trigger{" +
@@ -89,4 +394,3 @@ public class Trigger implements Serializable {
'}'; '}';
} }
} }

View File

@@ -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);
}
}

View File

@@ -39,19 +39,49 @@ import java.util.concurrent.atomic.AtomicBoolean;
public class TriggerScheduler { public class TriggerScheduler {
private static final Logger log = LoggerFactory.getLogger(TriggerScheduler.class); 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 TriggerStore store;
private final ScheduledExecutorService scheduler; private final ScheduledExecutorService scheduler;
private final ExecutorService worker; 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); private final AtomicBoolean closed = new AtomicBoolean(false);
// map 用于管理取消triggerId -> ScheduledFuture // map 用于管理取消triggerId -> ScheduledFuture
private final ConcurrentMap<String, ScheduledFuture<?>> scheduledFutures = new ConcurrentHashMap<>(); 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去处理 // consumer 来把 trigger 交给 ChainExecutor或 ChainRuntime去处理
private volatile TriggerConsumer consumer; private volatile TriggerConsumer consumer;
private volatile TriggerFailureListener failureListener;
// 周期扫查间隔ms // 周期扫查间隔ms
private final long scanIntervalMs; private final long scanIntervalMs;
private final long localScheduleHorizonMs;
// 扫描任务 future // 扫描任务 future
private ScheduledFuture<?> scanFuture; private ScheduledFuture<?> scanFuture;
@@ -60,13 +90,34 @@ public class TriggerScheduler {
void accept(Trigger trigger, ExecutorService worker); 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) { public TriggerScheduler(TriggerStore store, ScheduledExecutorService scheduler, ExecutorService worker, long scanIntervalMs) {
this.store = Objects.requireNonNull(store, "TriggerStore required"); this.store = Objects.requireNonNull(store, "TriggerStore required");
this.scheduler = Objects.requireNonNull(scheduler, "ScheduledExecutorService required"); this.scheduler = Objects.requireNonNull(scheduler, "ScheduledExecutorService required");
this.worker = Objects.requireNonNull(worker, "ExecutorService required"); this.worker = Objects.requireNonNull(worker, "ExecutorService required");
this.dispatchPermits = createDispatchPermits(worker);
this.scanIntervalMs = Math.max(1000, scanIntervalMs); 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(); recoverAndSchedulePending();
// 启动周期扫查 findDue // 启动周期扫查 findDue
@@ -78,6 +129,40 @@ public class TriggerScheduler {
this.consumer = consumer; 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 (单机语义) * schedule a trigger: persist -> schedule (单机语义)
*/ */
@@ -86,20 +171,55 @@ public class TriggerScheduler {
if (trigger.getId() == null) { if (trigger.getId() == null) {
trigger.setId(UUID.randomUUID().toString()); trigger.setId(UUID.randomUUID().toString());
} }
if (trigger.getLogicalExecutionId() == null || trigger.getLogicalExecutionId().isBlank()) {
trigger.setLogicalExecutionId(trigger.getId());
}
store.save(trigger); store.save(trigger);
scheduleInternal(trigger); scheduleInternal(trigger);
return 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) * cancel trigger (从 store 删除并尝试取消已 schedule 的 future)
*/ */
public boolean cancel(String triggerId) { public boolean cancel(String triggerId) {
boolean removed = store.remove(triggerId); boolean removed = store.remove(triggerId);
ScheduledFuture<?> f = scheduledFutures.remove(triggerId); removeLocalSchedule(triggerId, true);
if (f != null) {
f.cancel(false);
}
return removed; return removed;
} }
@@ -107,83 +227,185 @@ public class TriggerScheduler {
* 主动触发webhook/event/manual 场景) * 主动触发webhook/event/manual 场景)
*/ */
public boolean fire(String triggerId) { public boolean fire(String triggerId) {
if (closed.get()) return false; if (closed.get()) {
Trigger t = store.find(triggerId);
if (t == null) return false;
if (consumer == null) {
// 无 consumer仍从 store 中移除
store.remove(triggerId);
return false; return false;
} }
// 在 worker 线程触发 consumer Trigger candidate = store.find(triggerId);
worker.submit(() -> { if (candidate == null) {
try { return false;
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);
} }
}); removeLocalSchedule(triggerId, true);
return true; return claimAndDispatch(candidate);
} }
/** /**
* internal scheduling for a trigger (单机 scheduled semantics) * internal scheduling for a trigger (单机 scheduled semantics)
*/ */
private void scheduleInternal(Trigger trigger) { private void scheduleInternal(Trigger trigger) {
if (closed.get()) return; if (closed.get()) {
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());
return; return;
} }
long now = System.currentTimeMillis();
if (consumer != null) { if (trigger.getTriggerAt()
worker.submit(() -> { > scheduleHorizonTimestamp(now)) {
try { return;
TriggerContext.setCurrentTrigger(existing); }
consumer.accept(existing, worker); synchronized (localScheduleMonitor) {
} catch (Throwable e) { if (closed.get()) {
log.error(e.toString(), e); return;
} finally { }
TriggerContext.clearCurrentTrigger(); ScheduledFuture<?> existing;
store.remove(existing.getId()); while ((existing = scheduledFutures.get(trigger.getId())) != null) {
scheduledFutures.remove(existing.getId()); 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() { private void recoverAndSchedulePending() {
try { try {
List<Trigger> list = store.findAllPending(); List<Trigger> list = store.findDue(
scheduleHorizonTimestamp());
if (list == null || list.isEmpty()) return; if (list == null || list.isEmpty()) return;
for (Trigger t : list) { for (Trigger t : list) {
scheduleInternal(t); scheduleInternal(t);
} }
} catch (Throwable t) { } catch (Throwable t) {
// 忽略单次恢复错误,继续运行 log.error("Failed to recover pending workflow triggers", t);
t.printStackTrace();
} }
} }
@@ -191,7 +413,7 @@ public class TriggerScheduler {
if (closed.get()) return; if (closed.get()) return;
scanFuture = scheduler.scheduleAtFixedRate(() -> { scanFuture = scheduler.scheduleAtFixedRate(() -> {
try { try {
long upto = System.currentTimeMillis(); long upto = scheduleHorizonTimestamp();
List<Trigger> due = store.findDue(upto); List<Trigger> due = store.findDue(upto);
if (due == null || due.isEmpty()) return; if (due == null || due.isEmpty()) return;
for (Trigger t : due) { for (Trigger t : due) {
@@ -200,27 +422,363 @@ public class TriggerScheduler {
if (sf != null && !sf.isDone() && !sf.isCancelled()) { if (sf != null && !sf.isDone() && !sf.isCancelled()) {
continue; continue;
} }
// 直接提交到 worker让 consumer 处理;并从 store 中移除 scheduleInternal(t);
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());
}
} }
} catch (Throwable tt) { } catch (Throwable tt) {
tt.printStackTrace(); log.error("Failed to scan due workflow triggers", tt);
} }
}, scanIntervalMs, scanIntervalMs, TimeUnit.MILLISECONDS); }, 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() { public void shutdown() {
if (closed.compareAndSet(false, true)) { if (closed.compareAndSet(false, true)) {
if (scanFuture != null) scanFuture.cancel(false); if (scanFuture != null) scanFuture.cancel(false);
@@ -232,6 +790,11 @@ public class TriggerScheduler {
} }
} }
scheduledFutures.clear(); scheduledFutures.clear();
scheduledTriggerTimes.clear();
for (ScheduledFuture<?> renewal : claimRenewals.values()) {
renewal.cancel(false);
}
claimRenewals.clear();
try { try {
scheduler.shutdownNow(); scheduler.shutdownNow();
@@ -241,6 +804,18 @@ public class TriggerScheduler {
worker.shutdownNow(); worker.shutdownNow();
} catch (Throwable ignored) { } 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();
} }
} }
} }

View File

@@ -18,8 +18,27 @@ package com.easyagents.flow.core.chain.runtime;
import java.util.List; import java.util.List;
public interface TriggerStore { public interface TriggerStore {
Trigger save(Trigger trigger); 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); boolean remove(String triggerId);
Trigger find(String triggerId); Trigger find(String triggerId);
@@ -27,4 +46,116 @@ public interface TriggerStore {
List<Trigger> findDue(long uptoTimestamp); List<Trigger> findDue(long uptoTimestamp);
List<Trigger> findAllPending(); 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);
}
} }

View File

@@ -16,6 +16,8 @@
package com.easyagents.flow.core.code.impl; package com.easyagents.flow.core.code.impl;
import com.easyagents.flow.core.chain.Chain; 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.code.CodeRuntimeEngine;
import com.easyagents.flow.core.node.CodeNode; import com.easyagents.flow.core.node.CodeNode;
import com.easyagents.flow.core.util.graalvm.JsInteropUtils; 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) { public Map<String, Object> execute(String code, CodeNode node, Chain chain) {
try (Context context = CONTEXT_BUILDER.build()) { try (Context context = CONTEXT_BUILDER.build()) {
Value bindings = context.getBindings("js"); 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) -> { all.forEach((key, value) -> {
if (!key.contains(".")) { if (!key.contains(".")) {
bindings.putMember(key, JsInteropUtils.wrapJavaValueForJS(context, value)); 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) { if (parameterValues != null) {
for (Map.Entry<String, Object> entry : parameterValues.entrySet()) { for (Map.Entry<String, Object> entry : parameterValues.entrySet()) {
bindings.putMember(entry.getKey(), JsInteropUtils.wrapJavaValueForJS(context, entry.getValue())); bindings.putMember(entry.getKey(), JsInteropUtils.wrapJavaValueForJS(context, entry.getValue()));
} }
} }
bindings.putMember("_chain", chain);
bindings.putMember("_state", chain.getNodeState(node.getId()));
// 在 JS 中创建 _result 对象 // 在 JS 中创建 _result 对象
context.eval("js", "var _result = {};"); context.eval("js", "var _result = {};");
// 注入 _chain 和 _context // 注入 _chain 和 _context
bindings.putMember("_chain", chain); bindings.putMember("_chain", chain);
bindings.putMember("_state", chain.getNodeState(node.getId())); bindings.putMember("_state", nodeState);
// 执行用户脚本 // 执行用户脚本
context.eval("js", code); context.eval("js", code);

View File

@@ -85,6 +85,7 @@ public interface Llm {
* 实现了Serializable接口支持序列化 * 实现了Serializable接口支持序列化
*/ */
class ChatOptions implements Serializable { class ChatOptions implements Serializable {
private static final long serialVersionUID = 1L;
private String seed; private String seed;
private Float temperature = 0.8f; private Float temperature = 0.8f;

View File

@@ -23,6 +23,7 @@ import java.util.Collection;
import java.util.List; import java.util.List;
public abstract class BaseNode extends Node { public abstract class BaseNode extends Node {
private static final long serialVersionUID = 1L;
protected List<Parameter> parameters; protected List<Parameter> parameters;
protected List<Parameter> outputDefs; protected List<Parameter> outputDefs;

View File

@@ -27,6 +27,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
public class CodeNode extends BaseNode { public class CodeNode extends BaseNode {
private static final long serialVersionUID = 1L;
protected String engine; protected String engine;
protected String code; protected String code;
@@ -52,7 +54,8 @@ public class CodeNode extends BaseNode {
throw new IllegalArgumentException("code is empty"); throw new IllegalArgumentException("code is empty");
} }
ChainState chainState = chain.getState(); ChainState chainState =
chain.getExecutionState();
Map<String, Object> parameterValues = chainState.resolveParameters(this); Map<String, Object> parameterValues = chainState.resolveParameters(this);
String newCode = TextTemplate.of(code).formatToString(chainState.buildTemplateRootMaps(parameterValues)); String newCode = TextTemplate.of(code).formatToString(chainState.buildTemplateRootMaps(parameterValues));

View File

@@ -25,6 +25,8 @@ import com.easyagents.flow.core.chain.repository.ChainStateField;
import java.util.*; import java.util.*;
public class ConfirmNode extends BaseNode { public class ConfirmNode extends BaseNode {
private static final long serialVersionUID = 1L;
private String message; private String message;
private List<Parameter> confirms; private List<Parameter> confirms;
@@ -70,7 +72,8 @@ public class ConfirmNode extends BaseNode {
Map<String, Object> values; Map<String, Object> values;
try { try {
values = chain.getState().resolveParameters(this, confirmParameters); values = chain.getExecutionState()
.resolveParameters(this, confirmParameters);
// 移除 confirm 参数,方便在其他节点二次确认,或者在 for 循环中第二次获取 // 移除 confirm 参数,方便在其他节点二次确认,或者在 for 循环中第二次获取
chain.updateStateSafely(state -> { chain.updateStateSafely(state -> {
for (Parameter confirmParameter : confirmParameters) { for (Parameter confirmParameter : confirmParameters) {
@@ -94,7 +97,12 @@ public class ConfirmNode extends BaseNode {
} }
// 获取参数值,不会触发 ChainSuspendException 错误 // 获取参数值,不会触发 ChainSuspendException 错误
Map<String, Object> parameterValues = chain.getState().resolveParameters(this, newParameters, null, true); Map<String, Object> parameterValues =
chain.getExecutionState().resolveParameters(
this,
newParameters,
null,
true);
// 设置 enums方便前端给用户进行选择 // 设置 enums方便前端给用户进行选择
for (Parameter confirmParameter : confirmParameters) { for (Parameter confirmParameter : confirmParameters) {

View File

@@ -22,6 +22,8 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
public class EndNode extends BaseNode { public class EndNode extends BaseNode {
private static final long serialVersionUID = 1L;
private boolean normal = true; private boolean normal = true;
private String message; private String message;
@@ -47,7 +49,8 @@ public class EndNode extends BaseNode {
@Override @Override
public Map<String, Object> execute(Chain chain) { public Map<String, Object> execute(Chain chain) {
ChainState chainState =
chain.getExecutionState();
Map<String, Object> output = new HashMap<>(); Map<String, Object> output = new HashMap<>();
if (normal) { if (normal) {
output.put(ChainConsts.CHAIN_STATE_STATUS_KEY, ChainStatus.SUCCEEDED); output.put(ChainConsts.CHAIN_STATE_STATUS_KEY, ChainStatus.SUCCEEDED);
@@ -62,7 +65,7 @@ public class EndNode extends BaseNode {
if (this.outputDefs != null) { if (this.outputDefs != null) {
for (Parameter outputDef : this.outputDefs) { for (Parameter outputDef : this.outputDefs) {
if (outputDef.getRefType() == RefType.REF) { 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) { } else if (outputDef.getRefType() == RefType.INPUT) {
output.put(outputDef.getName(), outputDef.getRef()); output.put(outputDef.getName(), outputDef.getRef());
} else if (outputDef.getRefType() == RefType.FIXED) { } else if (outputDef.getRefType() == RefType.FIXED) {
@@ -70,7 +73,7 @@ public class EndNode extends BaseNode {
} }
// default is ref type // default is ref type
else if (StringUtil.hasText(outputDef.getRef())) { else if (StringUtil.hasText(outputDef.getRef())) {
output.put(outputDef.getName(), chain.getState().resolveValue(outputDef.getRef())); output.put(outputDef.getName(), chainState.resolveValue(outputDef.getRef()));
} }
} }
} }

View File

@@ -22,21 +22,33 @@ import com.easyagents.flow.core.chain.DataType;
import com.easyagents.flow.core.chain.Parameter; import com.easyagents.flow.core.chain.Parameter;
import com.easyagents.flow.core.filestoreage.FileStorage; import com.easyagents.flow.core.filestoreage.FileStorage;
import com.easyagents.flow.core.filestoreage.FileStorageManager; 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.OkHttpClientUtil;
import com.easyagents.flow.core.util.StringUtil; import com.easyagents.flow.core.util.StringUtil;
import com.easyagents.flow.core.util.TextTemplate; import com.easyagents.flow.core.util.TextTemplate;
import okhttp3.*; import okhttp3.*;
import java.io.ByteArrayOutputStream;
import java.io.FilterInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
public class HttpNode extends BaseNode { 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 url;
private String method; private String method;
@@ -145,7 +157,7 @@ public class HttpNode extends BaseNode {
@Override @Override
public Map<String, Object> execute(Chain chain) { public Map<String, Object> execute(Chain chain) {
int maxRetry = 5; int maxRetry = supportsAutomaticRetry(method) ? 5 : 1;
long retryInterval = 2000L; long retryInterval = 2000L;
int attempt = 0; int attempt = 0;
@@ -161,7 +173,7 @@ public class HttpNode extends BaseNode {
lastError = ex; lastError = ex;
// 判断是否需要重试 // 判断是否需要重试
if (!shouldRetry(ex)) { if (attempt >= maxRetry || !shouldRetry(ex)) {
throw wrapAsRuntime(ex, attempt); throw wrapAsRuntime(ex, attempt);
} }
@@ -199,6 +211,19 @@ public class HttpNode extends BaseNode {
return cause instanceof IOException; 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) { private RuntimeException wrapAsRuntime(Throwable ex, int attempt) {
if (ex instanceof RuntimeException) { if (ex instanceof RuntimeException) {
return (RuntimeException) ex; return (RuntimeException) ex;
@@ -212,13 +237,18 @@ public class HttpNode extends BaseNode {
public Map<String, Object> doExecute(Chain chain) throws IOException { 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) String newUrl = TextTemplate.of(url)
.formatToString(chain.getState().buildTemplateRootMaps(argsMap)); .formatToString(
chain.getExecutionState()
.buildTemplateRootMaps(argsMap));
Request.Builder reqBuilder = new Request.Builder().url(newUrl); 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))); headersMap.forEach((s, o) -> reqBuilder.addHeader(s, String.valueOf(o)));
if (StringUtil.noText(method) || "GET".equalsIgnoreCase(method)) { if (StringUtil.noText(method) || "GET".equalsIgnoreCase(method)) {
@@ -227,8 +257,12 @@ public class HttpNode extends BaseNode {
reqBuilder.method(method.toUpperCase(), getRequestBody(chain, argsMap)); 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) { if (response.code() >= 500 && response.code() < 600) {
@@ -263,17 +297,39 @@ public class HttpNode extends BaseNode {
} }
if (bodyDataType == null) { 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")) { } 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) { } 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(); 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); result.put("body", fileUrl);
} }
}
} else { } else {
result.put("body", body.string()); try (IoBulkhead.Permit ignored =
IoBulkhead.responseAggregation().acquire(
IoBulkhead.targetForUrl(newUrl))) {
result.put("body", readTextBody(
body, resolveMaxTextResponseBytes()));
}
} }
return result; return result;
} }
@@ -282,19 +338,26 @@ public class HttpNode extends BaseNode {
private RequestBody getRequestBody(Chain chain, Map<String, Object> formatArgs) { private RequestBody getRequestBody(Chain chain, Map<String, Object> formatArgs) {
if ("json".equals(bodyType)) { if ("json".equals(bodyType)) {
String bodyJsonString = TextTemplate.of(bodyJson) String bodyJsonString = TextTemplate.of(bodyJson)
.formatToString(chain.getState().buildTemplateContextMap(formatArgs), true); .formatToString(
chain.getExecutionState()
.buildTemplateRootMaps(formatArgs),
true);
JSONObject jsonObject = JSON.parseObject(bodyJsonString); JSONObject jsonObject = JSON.parseObject(bodyJsonString);
return RequestBody.create(jsonObject.toString(), MediaType.parse("application/json")); return RequestBody.create(jsonObject.toString(), MediaType.parse("application/json"));
} }
if ("x-www-form-urlencoded".equals(bodyType)) { 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); String bodyString = mapToQueryString(formUrlencodedMap);
return RequestBody.create(bodyString, MediaType.parse("application/x-www-form-urlencoded")); return RequestBody.create(bodyString, MediaType.parse("application/x-www-form-urlencoded"));
} }
if ("form-data".equals(bodyType)) { 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() MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM); .setType(MultipartBody.FORM);
@@ -320,13 +383,97 @@ public class HttpNode extends BaseNode {
if ("raw".equals(bodyType)) { if ("raw".equals(bodyType)) {
String rawBodyString = TextTemplate.of(rawBody) String rawBodyString = TextTemplate.of(rawBody)
.formatToString(chain.getState().buildTemplateRootMaps(formatArgs)); .formatToString(
chain.getExecutionState()
.buildTemplateRootMaps(formatArgs));
return RequestBody.create(rawBodyString, null); return RequestBody.create(rawBodyString, null);
} }
//none //none
return RequestBody.create("", null); 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 { public static class HttpServerErrorException extends IOException {
private final int statusCode; 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 @Override
public String toString() { public String toString() {

View File

@@ -16,6 +16,7 @@
package com.easyagents.flow.core.node; package com.easyagents.flow.core.node;
import com.easyagents.flow.core.chain.Chain; 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.Knowledge;
import com.easyagents.flow.core.knowledge.KnowledgeManager; import com.easyagents.flow.core.knowledge.KnowledgeManager;
import com.easyagents.flow.core.util.Maps; import com.easyagents.flow.core.util.Maps;
@@ -29,6 +30,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
public class KnowledgeNode extends BaseNode { public class KnowledgeNode extends BaseNode {
private static final long serialVersionUID = 1L;
private static final Logger logger = org.slf4j.LoggerFactory.getLogger(KnowledgeNode.class); private static final Logger logger = org.slf4j.LoggerFactory.getLogger(KnowledgeNode.class);
@@ -71,11 +74,16 @@ public class KnowledgeNode extends BaseNode {
@Override @Override
public Map<String, Object> execute(Chain chain) { 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) String realKeyword = TextTemplate.of(keyword)
.formatToString(chain.getState().buildTemplateRootMaps(argsMap)); .formatToString(templateRootMaps);
String realLimitString = TextTemplate.of(limit) String realLimitString = TextTemplate.of(limit)
.formatToString(chain.getState().buildTemplateRootMaps(argsMap)); .formatToString(templateRootMaps);
int realLimit = 10; int realLimit = 10;
if (StringUtil.hasText(realLimitString)) { if (StringUtil.hasText(realLimitString)) {
try { try {

View File

@@ -17,6 +17,7 @@ package com.easyagents.flow.core.node;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.easyagents.flow.core.chain.Chain; 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.chain.Parameter;
import com.easyagents.flow.core.llm.Llm; import com.easyagents.flow.core.llm.Llm;
import com.easyagents.flow.core.llm.LlmManager; import com.easyagents.flow.core.llm.LlmManager;
@@ -26,6 +27,8 @@ import java.io.File;
import java.util.*; import java.util.*;
public class LlmNode extends BaseNode { public class LlmNode extends BaseNode {
private static final long serialVersionUID = 1L;
protected String llmId; protected String llmId;
protected Llm.ChatOptions chatOptions; protected Llm.ChatOptions chatOptions;
@@ -88,14 +91,20 @@ public class LlmNode extends BaseNode {
@Override @Override
public Map<String, Object> execute(Chain chain) { 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)) { if (StringUtil.noText(userPrompt)) {
throw new RuntimeException("Can not find user prompt"); throw new RuntimeException("Can not find user prompt");
} }
List<Map<String, Object>> templateRootMaps =
chainState.buildTemplateRootMaps(
parameterValues);
String userPromptString = TextTemplate.of(userPrompt) String userPromptString = TextTemplate.of(userPrompt)
.formatToString(chain.getState().buildTemplateRootMaps(parameterValues)); .formatToString(templateRootMaps);
Llm llm = LlmManager.getInstance().getChatModel(this.llmId); Llm llm = LlmManager.getInstance().getChatModel(this.llmId);
@@ -104,14 +113,16 @@ public class LlmNode extends BaseNode {
} }
String systemPromptString = TextTemplate.of(this.systemPrompt) String systemPromptString = TextTemplate.of(this.systemPrompt)
.formatToString(chain.getState().buildTemplateRootMaps(parameterValues)); .formatToString(templateRootMaps);
Llm.MessageInfo messageInfo = new Llm.MessageInfo(); Llm.MessageInfo messageInfo = new Llm.MessageInfo();
messageInfo.setMessage(userPromptString); messageInfo.setMessage(userPromptString);
messageInfo.setSystemMessage(systemPromptString); messageInfo.setSystemMessage(systemPromptString);
if (images != null && !images.isEmpty()) { 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<>(); List<String> imagesUrls = new ArrayList<>();
filesMap.forEach((s, o) -> { filesMap.forEach((s, o) -> {
if (o instanceof String) { if (o instanceof String) {

View File

@@ -18,19 +18,30 @@ package com.easyagents.flow.core.node;
import com.easyagents.flow.core.chain.*; import com.easyagents.flow.core.chain.*;
import com.easyagents.flow.core.chain.repository.ChainStateField; 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.repository.NodeStateField;
import com.easyagents.flow.core.chain.runtime.Trigger; import com.easyagents.flow.core.chain.runtime.Trigger;
import com.easyagents.flow.core.chain.runtime.TriggerContext; import com.easyagents.flow.core.chain.runtime.TriggerContext;
import com.easyagents.flow.core.chain.runtime.TriggerType; 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.Maps;
import com.easyagents.flow.core.util.StringUtil; import com.easyagents.flow.core.util.StringUtil;
import java.io.Serializable; import java.io.Serializable;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class LoopNode extends BaseNode { 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; private Parameter loopVar;
@@ -44,6 +55,43 @@ public class LoopNode extends BaseNode {
@Override @Override
public Map<String, Object> execute(Chain chain) { 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(); Trigger prevTrigger = TriggerContext.getCurrentTrigger();
Deque<LoopContext> loopStack = getOrCreateLoopStack(chain); Deque<LoopContext> loopStack = getOrCreateLoopStack(chain);
@@ -51,15 +99,22 @@ public class LoopNode extends BaseNode {
// 判断是否是首次进入该 LoopNode即不是由子节点返回 // 判断是否是首次进入该 LoopNode即不是由子节点返回
TriggerType triggerType = prevTrigger.getType(); TriggerType triggerType = prevTrigger.getType();
boolean isFirstEntry = triggerType != TriggerType.PARENT && triggerType != TriggerType.SELF; boolean isFirstEntry = !resumeAfterMaterialization
&& triggerType != TriggerType.PARENT
&& triggerType != TriggerType.SELF;
if (isFirstEntry) { if (isFirstEntry) {
// 首次触发:创建新的 LoopContext 并压入堆栈 // 首次触发:创建新的 LoopContext 并压入堆栈
loopContext = new LoopContext(); loopContext = new LoopContext();
loopContext.currentIndex = 0; loopContext.currentIndex = 0;
loopContext.subResult = new HashMap<>(); loopContext.resultId = chain.getStateInstanceId() + ":" + UUID.randomUUID();
// 保存原始触发上下文(用于循环结束后恢复) // 保存原始触发上下文(用于循环结束后恢复)
loopStack.offerLast(loopContext); 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 -> { chain.updateNodeStateSafely(this.id, state -> {
state.getMemory().put(buildLoopStackId(), loopStack); state.getMemory().put(buildLoopStackId(), loopStack);
@@ -83,33 +138,114 @@ public class LoopNode extends BaseNode {
loopContext = loopStack.peekFirst(); 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); migrateLegacyResult(chain, loopContext);
// int triggerLoopIndex = getTriggerLoopIndex(prevTrigger); if (!acceptParentBranch(prevTrigger, loopContext, chain, loopStack)) {
// return Maps.of(ChainConsts.SCHEDULE_NEXT_NODE_DISABLED_KEY, true)
// if (loopContext.currentIndex != triggerLoopIndex) { .set(ChainConsts.NODE_STATE_STATUS_KEY, NodeStatus.RUNNING);
// // 不执行,子流程有分叉,已经被其他的分叉节点触发了 }
// 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());
int shouldLoopCount; int shouldLoopCount;
if (loopValue instanceof Iterable) { boolean storedIterable = false;
shouldLoopCount = IterableUtil.size((Iterable<?>) loopValue); boolean directlyIndexed = false;
} else if (loopValue instanceof Number || (loopValue instanceof String && StringUtil.isNumeric(loopValue.toString()))) { boolean numericLoop = false;
shouldLoopCount = loopValue instanceof Number ? ((Number) loopValue).intValue() : Integer.parseInt(loopValue.toString().trim()); Object loopValue = null;
if (loopContext.iterableInputStored) {
// 已物化输入不再解析原始参数,避免大集合在每轮反序列化和路径解析。
shouldLoopCount = loopContext.iterableSize;
storedIterable = true;
} else { } 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 // 不是第一次执行,合并结果到 subResult
if (loopContext.currentIndex != 0) { if (loopContext.currentIndex != 0) {
ChainState subState = chain.getState(); ChainState subState =
chain.getExecutionState();
Map<String, Object> currentOutputs = collectCurrentOutputValues(subState); 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); publishLoopProgress(chain, currentOutputs);
} }
@@ -128,25 +264,37 @@ public class LoopNode extends BaseNode {
if (!loopStack.isEmpty()) { if (!loopStack.isEmpty()) {
chain.scheduleNode(this, null, TriggerType.SELF, 0); 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; int loopIndex = loopContext.currentIndex;
loopContext.currentIndex++; loopContext.currentIndex++;
chain.updateNodeStateSafely(this.id, state -> { persistLoopStack(chain, loopStack);
state.getMemory().put(buildLoopStackId(), loopStack);
return EnumSet.of(NodeStateField.MEMORY);
});
if (loopValue instanceof Iterable) { if (storedIterable) {
Object loopItem = IterableUtil.get((Iterable<?>) loopValue, loopIndex); Object loopItem = chain.getLoopResultRepository().loadInputItem(loopContext.resultId, loopIndex);
executeLoopChain(chain, loopContext, loopItem); 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); executeLoopChain(chain, loopContext, loopIndex);
} else { } 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); .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 1300 的循环总次数
* @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 实例独立) * 获取或创建当前节点的 LoopContext 堆栈(每个 LoopNode 实例独立)
@@ -168,15 +591,10 @@ public class LoopNode extends BaseNode {
stack = (Deque<LoopContext>) stackObj; stack = (Deque<LoopContext>) stackObj;
} else { } else {
stack = new ArrayDeque<>(); stack = new ArrayDeque<>();
chain.updateNodeStateSafely(this.id, state -> {
state.getMemory().put(key, stack);
return EnumSet.of(NodeStateField.MEMORY);
});
} }
return stack; return stack;
} }
private void executeLoopChain(Chain chain, LoopContext loopContext, Object loopItem) { private void executeLoopChain(Chain chain, LoopContext loopContext, Object loopItem) {
chain.updateStateSafely(state -> { chain.updateStateSafely(state -> {
@@ -188,13 +606,116 @@ public class LoopNode extends BaseNode {
ChainDefinition definition = chain.getDefinition(); ChainDefinition definition = chain.getDefinition();
List<Edge> outwardEdges = definition.getOutwardEdge(this.id); List<ChainDefinition.LoopChildDispatch> childDispatches =
for (Edge edge : outwardEdges) { definition.getLoopChildDispatches(this.id);
Node childNode = definition.getNodeById(edge.getTarget()); if (childDispatches.isEmpty()) {
if (childNode.getParentId() != null && childNode.getParentId().equals(this.id)) { throw new IllegalStateException("Loop node has no executable child branch: " + this.id);
chain.scheduleNode(childNode, edge.getId(), TriggerType.CHILD, 0); }
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(); List<Parameter> outputDefs = getOutputDefs();
if (outputDefs != null) { if (outputDefs != null) {
for (Parameter outputDef : outputDefs) { 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); if (value instanceof String) {
toResult.put(outputDef.getName(), existList); 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 { public static class LoopContext implements Serializable {
private static final long serialVersionUID = 5258356831772776243L;
int currentIndex; int currentIndex;
String resultId;
Map<String, Object> subResult; 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() { public int getCurrentIndex() {
return currentIndex; return currentIndex;
@@ -285,6 +867,14 @@ public class LoopNode extends BaseNode {
this.currentIndex = currentIndex; this.currentIndex = currentIndex;
} }
public String getResultId() {
return resultId;
}
public void setResultId(String resultId) {
this.resultId = resultId;
}
public Map<String, Object> getSubResult() { public Map<String, Object> getSubResult() {
return subResult; return subResult;
} }
@@ -293,5 +883,111 @@ public class LoopNode extends BaseNode {
this.subResult = subResult; 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;
}
} }
} }

View File

@@ -16,6 +16,7 @@
package com.easyagents.flow.core.node; package com.easyagents.flow.core.node;
import com.easyagents.flow.core.chain.Chain; 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.SearchEngine;
import com.easyagents.flow.core.searchengine.SearchEngineManager; import com.easyagents.flow.core.searchengine.SearchEngineManager;
import com.easyagents.flow.core.util.Maps; import com.easyagents.flow.core.util.Maps;
@@ -29,6 +30,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
public class SearchEngineNode extends BaseNode { public class SearchEngineNode extends BaseNode {
private static final long serialVersionUID = 1L;
private static final Logger logger = org.slf4j.LoggerFactory.getLogger(SearchEngineNode.class); private static final Logger logger = org.slf4j.LoggerFactory.getLogger(SearchEngineNode.class);
@@ -62,11 +65,16 @@ public class SearchEngineNode extends BaseNode {
@Override @Override
public Map<String, Object> execute(Chain chain) { 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) String realKeyword = TextTemplate.of(keyword)
.formatToString(chain.getState().buildTemplateRootMaps(argsMap)); .formatToString(templateRootMaps);
String realLimitString = TextTemplate.of(limit) String realLimitString = TextTemplate.of(limit)
.formatToString(chain.getState().buildTemplateRootMaps(argsMap)); .formatToString(templateRootMaps);
int realLimit = 10; int realLimit = 10;
if (StringUtil.hasText(realLimitString)) { if (StringUtil.hasText(realLimitString)) {
try { try {

View File

@@ -21,9 +21,12 @@ import com.easyagents.flow.core.chain.Chain;
import java.util.Map; import java.util.Map;
public class StartNode extends BaseNode { public class StartNode extends BaseNode {
private static final long serialVersionUID = 1L;
@Override @Override
public Map<String, Object> execute(Chain chain) { public Map<String, Object> execute(Chain chain) {
return chain.getState().resolveParameters(this); return chain.getExecutionState()
.resolveParameters(this);
} }
@Override @Override

View File

@@ -27,6 +27,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
public class TemplateNode extends BaseNode { public class TemplateNode extends BaseNode {
private static final long serialVersionUID = 1L;
private static final Engine engine; private static final Engine engine;
private String template; private String template;
@@ -48,7 +50,8 @@ public class TemplateNode extends BaseNode {
@Override @Override
public Map<String, Object> execute(Chain chain) { 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(); ByteArrayOutputStream result = new ByteArrayOutputStream();

View File

@@ -20,11 +20,13 @@ import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.easyagents.flow.core.chain.DataType; import com.easyagents.flow.core.chain.DataType;
import com.easyagents.flow.core.chain.JsCodeCondition; import com.easyagents.flow.core.chain.JsCodeCondition;
import com.easyagents.flow.core.chain.Node;
import com.easyagents.flow.core.chain.Parameter; import com.easyagents.flow.core.chain.Parameter;
import com.easyagents.flow.core.chain.RefType; import com.easyagents.flow.core.chain.RefType;
import com.easyagents.flow.core.node.BaseNode; import com.easyagents.flow.core.node.BaseNode;
import com.easyagents.flow.core.util.StringUtil; import com.easyagents.flow.core.util.StringUtil;
import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@@ -156,10 +158,7 @@ public abstract class BaseNodeParser<T extends BaseNode> implements NodeParser<T
} }
node.setLoopIntervalMs(loopIntervalMs); node.setLoopIntervalMs(loopIntervalMs);
Integer maxLoopCount = data.getInteger("maxLoopCount"); node.setMaxLoopCount(parseMaxLoopCount(data.get("maxLoopCount")));
if (maxLoopCount != null) {
node.setMaxLoopCount(maxLoopCount);
}
String loopBreakCondition = data.getString("loopBreakCondition"); String loopBreakCondition = data.getString("loopBreakCondition");
if (StringUtil.hasText(loopBreakCondition)) { if (StringUtil.hasText(loopBreakCondition)) {
@@ -201,5 +200,30 @@ public abstract class BaseNodeParser<T extends BaseNode> implements NodeParser<T
return node; return node;
} }
/**
* 解析节点循环总次数,并拒绝小数、零值和超出上限的配置。
*
* @param value 原始循环次数;为空时使用默认值 1
* @return 合法的循环总次数
* @throws IllegalArgumentException 循环次数不是 1300 的整数
*/
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); protected abstract T doParse(JSONObject nodeJSONObject, JSONObject data, JSONObject chainJSONObject);
} }

View File

@@ -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) {
}
}

View File

@@ -1,5 +1,6 @@
package com.easyagents.flow.core.util; package com.easyagents.flow.core.util;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@@ -61,4 +62,25 @@ public class IterableUtil {
throw new IndexOutOfBoundsException("index >= size: " + index); 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;
}
} }

View File

@@ -18,21 +18,84 @@ package com.easyagents.flow.core.util;
import com.easyagents.flow.core.chain.Chain; import com.easyagents.flow.core.chain.Chain;
import com.easyagents.flow.core.util.graalvm.JsInteropUtils; import com.easyagents.flow.core.util.graalvm.JsInteropUtils;
import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.HostAccess; import org.graalvm.polyglot.HostAccess;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value; import org.graalvm.polyglot.Value;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 在隔离的 GraalVM JavaScript 上下文中执行工作流条件表达式。
*/
public class JsConditionUtil { public class JsConditionUtil {
// 使用 Context.Builder 构建上下文,线程安全 private static final int SOURCE_CACHE_LIMIT = 2048;
private static final Context.Builder CONTEXT_BUILDER = Context.newBuilder("js") /**
* 单条超长动态表达式不进入共享缓存,限制源码字符串和编译元数据总占用。
*/
private static final int MAX_CACHEABLE_SOURCE_CHARS =
16 * 1024;
/**
* Engine 跨 Context 共享编译缓存Context 仍按每次求值独立创建。
*/
private static final Engine ENGINE = Engine.newBuilder()
.option("engine.WarnInterpreterOnly", "false") .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 对象的方法和字段 .allowHostAccess(HostAccess.ALL) // 允许访问 Java 对象的方法和字段
.allowHostClassLookup(className -> false) // 禁止动态加载任意 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 结果 * 执行 JavaScript 表达式并返回 boolean 结果
@@ -43,7 +106,7 @@ public class JsConditionUtil {
* @return true 表示满足条件继续执行false 表示跳过 * @return true 表示满足条件继续执行false 表示跳过
*/ */
public static boolean eval(String code, Chain chain, Map<String, Object> initMap) { 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<>(); Map<String, Object> _result = new HashMap<>();
Value bindings = context.getBindings("js"); Value bindings = context.getBindings("js");
@@ -54,9 +117,8 @@ public class JsConditionUtil {
}); });
bindings.putMember("_result", _result); bindings.putMember("_result", _result);
code = "_result.value = " + code;
context.eval("js", code); context.eval(source(code));
Object value = _result.get("value"); Object value = _result.get("value");
return toBoolean(value); return toBoolean(value);
} catch (Exception e) { } 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) { 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<>(); Map<String, Object> _result = new HashMap<>();
Value bindings = context.getBindings("js"); Value bindings = context.getBindings("js");
@@ -77,9 +147,8 @@ public class JsConditionUtil {
}); });
bindings.putMember("_result", _result); bindings.putMember("_result", _result);
code = "_result.value = " + code;
context.eval("js", code); context.eval(source(code));
Object value = _result.get("value"); Object value = _result.get("value");
return toLong(value); return toLong(value);
} catch (Exception e) { } catch (Exception e) {
@@ -89,7 +158,10 @@ public class JsConditionUtil {
/** /**
* 将任意对象安全转换为 long 类型 * 将任意对象安全转换为 long 类型
*
* @param value 待转换值
* @return 长整数结果
*/ */
private static long toLong(Object value) { private static long toLong(Object value) {
if (value == null) { 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) { 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 Memory 中的变量(去掉前缀)
chain.getState().getMemory().forEach((key, value) -> { chain.getExecutionState().getMemory().forEach((key, value) -> {
int dotIndex = key.indexOf("."); int dotIndex = key.indexOf(".");
String varName = (dotIndex >= 0) ? key.substring(dotIndex + 1) : key; String varName = (dotIndex >= 0) ? key.substring(dotIndex + 1) : key;
variables.put(varName, value); variables.put(varName, value);
@@ -155,7 +231,10 @@ public class JsConditionUtil {
} }
/** /**
* 将任意对象转换为布尔值 * 将任意对象转换为布尔值
*
* @param value 待转换值
* @return 布尔结果
*/ */
private static boolean toBoolean(Object value) { private static boolean toBoolean(Object value) {
if (value == null) { if (value == null) {

View File

@@ -16,6 +16,13 @@
package com.easyagents.flow.core.util; package com.easyagents.flow.core.util;
import okhttp3.OkHttpClient; 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.SSLContext;
import javax.net.ssl.SSLSocketFactory; 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 final Logger LOGGER = Logger.getLogger(OkHttpClientUtil.class.getName());
private static volatile OkHttpClient.Builder customBuilder; private static volatile OkHttpClient.Builder customBuilder;
private static volatile OkHttpClient sharedClient;
private static volatile OkHttpClient sharedNoRetryClient;
private static final Object LOCK = new Object(); private static final Object LOCK = new Object();
// Prevent instantiation // Prevent instantiation
@@ -58,7 +67,11 @@ public final class OkHttpClientUtil {
if (builder == null) { if (builder == null) {
throw new IllegalArgumentException("Builder must not be null"); throw new IllegalArgumentException("Builder must not be null");
} }
synchronized (LOCK) {
customBuilder = builder; customBuilder = builder;
sharedClient = null;
sharedNoRetryClient = null;
}
} }
/** /**
@@ -70,18 +83,19 @@ public final class OkHttpClientUtil {
* </p> * </p>
*/ */
public static OkHttpClient buildDefaultClient() { public static OkHttpClient buildDefaultClient() {
OkHttpClient.Builder builder = customBuilder; OkHttpClient client = sharedClient;
if (builder != null) { if (client != null) {
return builder.build(); return client;
} }
synchronized (LOCK) { synchronized (LOCK) {
// Double-check in case another thread set it while waiting client = sharedClient;
builder = customBuilder; if (client != null) {
if (builder != null) { return client;
return builder.build();
} }
OkHttpClient.Builder builder = customBuilder;
if (builder == null) {
builder = new OkHttpClient.Builder() builder = new OkHttpClient.Builder()
.connectTimeout(1, TimeUnit.MINUTES) .connectTimeout(1, TimeUnit.MINUTES)
.readTimeout(5, TimeUnit.MINUTES); .readTimeout(5, TimeUnit.MINUTES);
@@ -94,7 +108,48 @@ public final class OkHttpClientUtil {
} }
configureProxy(builder); 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; 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;
}
}
} }

View File

@@ -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 编译缓存,避免重复编译 * 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) { public static TextTemplate of(String template) {
String finalTemplate = template != null ? 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) { public String formatToString(List<Map<String, Object>> rootMaps) {
Map<String, Object> rootMap = new HashMap<>(); return formatToString(rootMaps, false);
for (Map<String, Object> m : rootMaps) {
if (m != null) {
rootMap.putAll(m);
} }
/**
* 使用分层上下文格式化模板,避免合并复制完整工作流状态。
* <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; 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); Object value = compiled.eval(root);
if (escapeForJsonOutput && value instanceof String) { if (escapeForJsonOutput && value instanceof String) {
return escapeJsonString((String) value); return escapeJsonString((String) value);
@@ -384,4 +422,159 @@ public class TextTemplate {
return new EvaluationResult(false, ""); 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;
}
}
} }

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -15,20 +15,31 @@
*/ */
package com.easyagents.flow.core.test; 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.ChainDefinition;
import com.easyagents.flow.core.chain.ChainStatus;
import com.easyagents.flow.core.chain.Edge; 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.InMemoryChainStateRepository;
import com.easyagents.flow.core.chain.repository.InMemoryNodeStateRepository; import com.easyagents.flow.core.chain.repository.InMemoryNodeStateRepository;
import com.easyagents.flow.core.chain.runtime.ChainExecutor; import com.easyagents.flow.core.chain.runtime.ChainExecutor;
import com.easyagents.flow.core.chain.runtime.InMemoryTriggerStore; import com.easyagents.flow.core.chain.runtime.InMemoryTriggerStore;
import com.easyagents.flow.core.chain.runtime.TriggerScheduler; import com.easyagents.flow.core.chain.runtime.TriggerScheduler;
import com.easyagents.flow.core.node.EndNode; import com.easyagents.flow.core.node.EndNode;
import com.easyagents.flow.core.node.BaseNode;
import com.easyagents.flow.core.node.StartNode; import com.easyagents.flow.core.node.StartNode;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import java.lang.reflect.Field;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
@@ -37,6 +48,7 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/** /**
* {@link ChainExecutor} 并发同步执行测试。 * {@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); definition.addEdge(edge);
return definition; 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);
}
}
} }

View File

@@ -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;
}
}

View File

@@ -1,13 +1,19 @@
package com.easyagents.flow.core.test; 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.ChainState;
import com.easyagents.flow.core.chain.Parameter; import com.easyagents.flow.core.chain.Parameter;
import com.easyagents.flow.core.chain.RefType; import com.easyagents.flow.core.chain.RefType;
import com.easyagents.flow.core.chain.repository.InMemoryLoopResultRepository;
import com.easyagents.flow.core.chain.repository.LoopInputReference;
import com.easyagents.flow.core.node.StartNode; import com.easyagents.flow.core.node.StartNode;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import java.lang.reflect.Field;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
@@ -32,4 +38,229 @@ public class ChainTemplateContextTest {
Assert.assertEquals("7", result.get("nextValue")); 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);
}
}
} }

View File

@@ -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());
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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"));
}
}

View File

@@ -3,8 +3,18 @@ package com.easyagents.flow.core.test;
import com.easyagents.flow.core.chain.*; import com.easyagents.flow.core.chain.*;
import com.easyagents.flow.core.chain.repository.ChainDefinitionRepository; import com.easyagents.flow.core.chain.repository.ChainDefinitionRepository;
import com.easyagents.flow.core.chain.repository.InMemoryChainStateRepository; 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.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.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.BaseNode;
import com.easyagents.flow.core.node.EndNode; import com.easyagents.flow.core.node.EndNode;
import com.easyagents.flow.core.node.LoopNode; 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.Assert;
import org.junit.Test; 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.Collections;
import java.util.EnumSet;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator;
import java.util.Map; 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 @Test
public void shouldExposeLatestLoopOutputInsideLoopBody() { 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(); ChainDefinition definition = new ChainDefinition();
definition.setId("loop-progress-test"); definition.setId("loop-progress-test");
@@ -67,17 +368,115 @@ public class LoopNodeProgressContextTest {
definition.addEdge(edge("e1", "start", "loop")); definition.addEdge(edge("e1", "start", "loop"));
definition.addEdge(edge("e2", "loop", "acc")); definition.addEdge(edge("e2", "loop", "acc"));
definition.addEdge(edge("e3", "loop", "end")); 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 InMemoryChainStateRepository(),
new InMemoryNodeStateRepository()); new InMemoryNodeStateRepository());
}
Map<String, Object> variables = new HashMap<>(); /**
variables.put("times", 2); * 断言循环节点通过结束事件报告指定异常类型。
*
Map<String, Object> resultMap = executor.execute("loop-progress-test", variables); * @param executor 测试执行器
* @param definitionId 定义 ID
Assert.assertEquals(java.util.Arrays.asList("1", "2"), resultMap.get("result")); * @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) { 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再计算新的结果。 * 每一轮都读取父循环节点上一轮的 current再计算新的结果。
*/ */
@@ -142,4 +706,31 @@ public class LoopNodeProgressContextTest {
return result; 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);
}
}
} }

View File

@@ -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();
}
}
}

View File

@@ -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));
}
}

View File

@@ -44,4 +44,66 @@ public class TextTemplatePathTest {
Assert.assertEquals("value=", result); 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);
}
} }

View File

@@ -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();
}
}
}