fix: 统一工作流三个出口的错误反馈
- 关联 EASY-2,补齐安全错误、节点展示与执行标识 - 保留历史可读摘要并验证接口、SSE 与界面兼容
This commit is contained in:
@@ -23,6 +23,10 @@ public class ChainInfo implements Serializable {
|
||||
* 消息,错误时显示
|
||||
*/
|
||||
private String message;
|
||||
private WorkflowExecutionError error;
|
||||
|
||||
public WorkflowExecutionError getError() { return error; }
|
||||
public void setError(WorkflowExecutionError error) { this.error = error; }
|
||||
/**
|
||||
* 执行结果
|
||||
*/
|
||||
|
||||
@@ -28,6 +28,10 @@ public class NodeInfo implements Serializable {
|
||||
* 消息,错误时显示
|
||||
*/
|
||||
private String message;
|
||||
private WorkflowExecutionError error;
|
||||
|
||||
public WorkflowExecutionError getError() { return error; }
|
||||
public void setError(WorkflowExecutionError error) { this.error = error; }
|
||||
/**
|
||||
* 执行结果
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package tech.easyflow.ai.easyagentsflow.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/** 三个运行出口共用的安全错误信息。 */
|
||||
public class WorkflowExecutionError implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final String code;
|
||||
private final String reasonCode;
|
||||
private final String message;
|
||||
private final String nodeId;
|
||||
private final String nodeName;
|
||||
private final boolean retryable;
|
||||
|
||||
public WorkflowExecutionError(String code, String reasonCode, String message,
|
||||
String nodeId, String nodeName, boolean retryable) {
|
||||
this.code = code;
|
||||
this.reasonCode = reasonCode;
|
||||
this.message = message;
|
||||
this.nodeId = nodeId;
|
||||
this.nodeName = nodeName;
|
||||
this.retryable = retryable;
|
||||
}
|
||||
|
||||
public String getCode() { return code; }
|
||||
public String getReasonCode() { return reasonCode; }
|
||||
public String getMessage() { return message; }
|
||||
public String getNodeId() { return nodeId; }
|
||||
public String getNodeName() { return nodeName; }
|
||||
public boolean isRetryable() { return retryable; }
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.easyagentsflow.event.WorkflowExecutionAuditEvent;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowExecutionErrorMapper;
|
||||
import tech.easyflow.ai.easyagentsflow.event.WorkflowExecutionAuditProducer;
|
||||
import tech.easyflow.ai.easyagentsflow.repository.FrozenWorkflowDefinitionRegistry;
|
||||
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
||||
@@ -111,7 +112,7 @@ public class ChainEventListenerForSave implements ChainEventListener {
|
||||
state.getExecuteResult()));
|
||||
ExceptionSummary error = state.getError();
|
||||
if (error != null) {
|
||||
record.setErrorInfo(error.getRootCauseClass() + " --> " + error.getRootCauseMessage());
|
||||
record.setErrorInfo(WorkflowExecutionErrorMapper.summary(WorkflowExecutionErrorMapper.chain(error, state.getStatus())));
|
||||
}
|
||||
sendAuditEvent(
|
||||
WorkflowExecutionAuditEvent.Type.CHAIN_ENDED,
|
||||
@@ -209,14 +210,13 @@ public class ChainEventListenerForSave implements ChainEventListener {
|
||||
step.setEndTime(new Date());
|
||||
step.setStatus(nodeStatus.getValue());
|
||||
ExceptionSummary error =
|
||||
event.getError() == null
|
||||
event.getErrorSummary() == null
|
||||
? (legacyNodeState == null
|
||||
? null
|
||||
: legacyNodeState.getError())
|
||||
: new ExceptionSummary(
|
||||
event.getError());
|
||||
: event.getErrorSummary();
|
||||
if (error != null) {
|
||||
step.setErrorInfo(error.getRootCauseClass() + " --> " + error.getRootCauseMessage());
|
||||
step.setErrorInfo(WorkflowExecutionErrorMapper.summary(WorkflowExecutionErrorMapper.node(error, nodeStatus, node.getId(), node.getName())));
|
||||
}
|
||||
sendAuditEvent(
|
||||
WorkflowExecutionAuditEvent.Type.NODE_ENDED,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package tech.easyflow.ai.easyagentsflow.service;
|
||||
|
||||
import com.easyagents.document.core.exception.DocumentParseException;
|
||||
import com.easyagents.flow.core.chain.ChainState;
|
||||
import com.easyagents.flow.core.chain.ExceptionSummary;
|
||||
import com.easyagents.flow.core.chain.NodeState;
|
||||
@@ -8,11 +7,9 @@ import com.easyagents.flow.core.chain.NodeStatus;
|
||||
import com.easyagents.flow.core.chain.repository.ChainStateRepository;
|
||||
import com.easyagents.flow.core.chain.repository.NodeStateRepository;
|
||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||
import com.easyagents.flow.core.code.impl.JavascriptExecutionException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
||||
import tech.easyflow.common.util.StringUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
@@ -59,10 +56,8 @@ public class TinyFlowService {
|
||||
? Map.of()
|
||||
: resolvedNodeNames;
|
||||
for (NodeInfo node : nodes) {
|
||||
if (node != null
|
||||
&& StringUtil.noText(node.getNodeName())) {
|
||||
node.setNodeName(nodeNames.get(node.getNodeId()));
|
||||
}
|
||||
if (node == null) continue;
|
||||
node.setNodeName(nodeNames.get(node.getNodeId()));
|
||||
processNodeState(executeId, node, chainState, nodeStateRepository);
|
||||
res.getNodes().put(node.getNodeId(), node);
|
||||
}
|
||||
@@ -100,9 +95,8 @@ public class TinyFlowService {
|
||||
res.setExecuteId(executeId);
|
||||
res.setStatus(chainState.getStatus().getValue());
|
||||
ExceptionSummary chainError = chainState.getError();
|
||||
if (chainError != null) {
|
||||
res.setMessage(formatError(chainError));
|
||||
}
|
||||
res.setError(WorkflowExecutionErrorMapper.chain(chainError, chainState.getStatus()));
|
||||
res.setMessage(WorkflowExecutionErrorMapper.summary(res.getError()));
|
||||
Map<String, Object> executeResult = chainState.getExecuteResult();
|
||||
if (executeResult != null && !executeResult.isEmpty()) {
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -127,12 +121,9 @@ public class TinyFlowService {
|
||||
? NodeStatus.READY.getValue()
|
||||
: nodeState.getStatus().getValue());
|
||||
|
||||
if (nodeState != null) {
|
||||
ExceptionSummary error = nodeState.getError();
|
||||
if (error != null) {
|
||||
node.setMessage(formatError(error));
|
||||
}
|
||||
}
|
||||
node.setError(nodeState == null ? null : WorkflowExecutionErrorMapper.node(
|
||||
nodeState.getError(), nodeState.getStatus(), nodeId, node.getNodeName(), chainState.getStatus().isTerminal()));
|
||||
node.setMessage(WorkflowExecutionErrorMapper.summary(node.getError()));
|
||||
|
||||
Map<String, Object> nodeExecuteResult = chainState.getNodeExecuteResult(nodeId);
|
||||
if (nodeExecuteResult != null && !nodeExecuteResult.isEmpty()) {
|
||||
@@ -151,34 +142,4 @@ public class TinyFlowService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将执行异常转换为试运行界面可读的错误信息。
|
||||
*
|
||||
* @param error 持久化的异常摘要
|
||||
* @return 可展示的错误信息
|
||||
*/
|
||||
private String formatError(ExceptionSummary error) {
|
||||
if (JavascriptExecutionException.class.getName()
|
||||
.equals(error.getExceptionClass())
|
||||
&& StringUtil.hasText(error.getMessage())) {
|
||||
return error.getMessage();
|
||||
}
|
||||
String rootClass = StringUtil.hasText(error.getRootCauseClass())
|
||||
? error.getRootCauseClass()
|
||||
: error.getExceptionClass();
|
||||
String rootMessage = StringUtil.hasText(error.getRootCauseMessage())
|
||||
? error.getRootCauseMessage()
|
||||
: error.getMessage();
|
||||
if (DocumentParseException.class.getName().equals(rootClass)
|
||||
&& StringUtil.hasText(rootMessage)) {
|
||||
return rootMessage;
|
||||
}
|
||||
if (StringUtil.noText(rootClass)) {
|
||||
return rootMessage;
|
||||
}
|
||||
if (StringUtil.noText(rootMessage)) {
|
||||
return rootClass;
|
||||
}
|
||||
return rootClass + " --> " + rootMessage;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package tech.easyflow.ai.easyagentsflow.service;
|
||||
|
||||
import com.easyagents.flow.core.chain.ChainStatus;
|
||||
import com.easyagents.flow.core.chain.ExceptionSummary;
|
||||
import com.easyagents.flow.core.chain.NodeStatus;
|
||||
import com.easyagents.flow.core.chain.WorkflowErrorReason;
|
||||
import com.easyagents.flow.core.chain.WorkflowExecutionException;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowExecutionError;
|
||||
import tech.easyflow.common.web.error.RequestErrorProfile;
|
||||
import tech.easyflow.common.web.error.WebErrorMapping;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
/** 只通过稳定原因码生成外部信息,原始 cause 与响应正文不进入展示或审计摘要。 */
|
||||
public final class WorkflowExecutionErrorMapper {
|
||||
private WorkflowExecutionErrorMapper() { }
|
||||
|
||||
public static WorkflowExecutionError chain(ExceptionSummary error, ChainStatus status) {
|
||||
if (status != ChainStatus.FAILED && status != ChainStatus.ERROR) return null;
|
||||
return map(error, true, null, null, status == ChainStatus.ERROR);
|
||||
}
|
||||
|
||||
public static WorkflowExecutionError node(ExceptionSummary error, NodeStatus status, String nodeId, String nodeName) {
|
||||
return node(error, status, nodeId, nodeName, false);
|
||||
}
|
||||
|
||||
public static WorkflowExecutionError node(ExceptionSummary error, NodeStatus status, String nodeId, String nodeName,
|
||||
boolean executionTerminal) {
|
||||
if (status != NodeStatus.FAILED && status != NodeStatus.ERROR) return null;
|
||||
return map(error, false, nodeId, nodeName, status == NodeStatus.ERROR && !executionTerminal);
|
||||
}
|
||||
|
||||
public static WorkflowExecutionError map(ExceptionSummary error, boolean workflow,
|
||||
String nodeId, String nodeName, boolean retryable) {
|
||||
if (error != null && error.getNodeId() != null) {
|
||||
nodeId = error.getNodeId();
|
||||
nodeName = error.getNodeName() == null ? nodeName : error.getNodeName();
|
||||
}
|
||||
return fromReason(error == null ? null : error.getErrorCode(), workflow, nodeId, nodeName, retryable);
|
||||
}
|
||||
|
||||
public static WorkflowExecutionError fromReason(String reasonCode, boolean workflow,
|
||||
String nodeId, String nodeName, boolean retryable) {
|
||||
WorkflowErrorReason reason = WorkflowErrorReason.fromCode(reasonCode);
|
||||
if (reason == null) {
|
||||
reason = nodeId == null ? WorkflowErrorReason.WORKFLOW_INTERNAL_ERROR : WorkflowErrorReason.NODE_EXECUTION_FAILED;
|
||||
}
|
||||
return new WorkflowExecutionError(workflow ? "WORKFLOW_EXECUTION_FAILED" : "NODE_EXECUTION_FAILED",
|
||||
reason.getCode(), reason.getDefaultMessage(), nodeId, nodeName, retryable);
|
||||
}
|
||||
|
||||
public static String summary(WorkflowExecutionError error) {
|
||||
if (error == null) return null;
|
||||
String name = error.getNodeName();
|
||||
return name == null || name.isBlank() ? error.getMessage() : "「" + name + "」:" + error.getMessage();
|
||||
}
|
||||
|
||||
/** 单节点同步执行沿用全局 HTTP 错误处理,并保留原请求的其他错误契约。 */
|
||||
public static void installRequestProfile() {
|
||||
if (!(RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes attributes)) return;
|
||||
var request = attributes.getRequest();
|
||||
Object previous = request.getAttribute(RequestErrorProfile.ATTRIBUTE_NAME);
|
||||
request.setAttribute(RequestErrorProfile.ATTRIBUTE_NAME, (RequestErrorProfile) (req, exception) -> {
|
||||
if (exception instanceof WorkflowExecutionException failure) {
|
||||
WorkflowExecutionError error = map(new ExceptionSummary(failure), false, null, null, false);
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("error", error);
|
||||
if (failure.getChainId() != null) data.put("executeId", failure.getChainId());
|
||||
return new WebErrorMapping(500, 500, error.getMessage(), data);
|
||||
}
|
||||
return previous instanceof RequestErrorProfile profile ? profile.map(req, exception) : null;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -485,7 +485,7 @@ public class WorkflowApiUploadLifecycleService {
|
||||
return new BusinessException(
|
||||
500,
|
||||
50001,
|
||||
"文件存储处理失败,请联系管理员并提供 requestId",
|
||||
"文件存储处理失败",
|
||||
error);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ package tech.easyflow.ai.easyagentsflow.service;
|
||||
|
||||
import com.easyagents.document.core.exception.DocumentParseException;
|
||||
import com.easyagents.flow.core.chain.ChainState;
|
||||
import com.easyagents.flow.core.chain.WorkflowErrorReason;
|
||||
import com.easyagents.flow.core.chain.WorkflowExecutionException;
|
||||
import com.easyagents.flow.core.chain.ChainStatus;
|
||||
import com.easyagents.flow.core.chain.ExceptionSummary;
|
||||
import com.easyagents.flow.core.chain.NodeState;
|
||||
@@ -205,7 +207,7 @@ public class TinyFlowServiceTest {
|
||||
* @throws Exception 测试依赖注入失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldExposeJavascriptExecutionMessage()
|
||||
public void shouldHideRawJavascriptExceptionDetails()
|
||||
throws Exception {
|
||||
ChainExecutor chainExecutor = mock(ChainExecutor.class);
|
||||
ChainStateRepository chainStateRepository =
|
||||
@@ -237,9 +239,9 @@ public class TinyFlowServiceTest {
|
||||
ChainInfo result = service.getChainStatus(
|
||||
EXECUTE_ID, List.of(node(NodeStatus.READY)));
|
||||
|
||||
Assert.assertEquals(message, result.getMessage());
|
||||
Assert.assertEquals(WorkflowErrorReason.WORKFLOW_INTERNAL_ERROR.getDefaultMessage(), result.getMessage());
|
||||
Assert.assertEquals(
|
||||
message,
|
||||
WorkflowErrorReason.WORKFLOW_INTERNAL_ERROR.getDefaultMessage(),
|
||||
result.getNodes().get(NODE_ID).getMessage());
|
||||
}
|
||||
|
||||
@@ -249,7 +251,7 @@ public class TinyFlowServiceTest {
|
||||
* @throws Exception 测试依赖注入失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldExposeDocumentParseMessageWithoutExceptionClass()
|
||||
public void shouldHideUnclassifiedDocumentCauseDetails()
|
||||
throws Exception {
|
||||
ChainExecutor chainExecutor = mock(ChainExecutor.class);
|
||||
ChainStateRepository chainStateRepository =
|
||||
@@ -274,7 +276,47 @@ public class TinyFlowServiceTest {
|
||||
|
||||
ChainInfo result = service.getChainStatus(EXECUTE_ID, null);
|
||||
|
||||
Assert.assertEquals(message, result.getMessage());
|
||||
Assert.assertEquals(WorkflowErrorReason.WORKFLOW_INTERNAL_ERROR.getDefaultMessage(), result.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldKeepFailureNodeWithEmptyNodeSelection() throws Exception {
|
||||
ChainExecutor executor = mock(ChainExecutor.class);
|
||||
ChainStateRepository states = mock(ChainStateRepository.class);
|
||||
when(executor.getChainStateRepository()).thenReturn(states);
|
||||
ChainState state = new ChainState();
|
||||
state.setStatus(ChainStatus.FAILED);
|
||||
state.setError(new ExceptionSummary(new WorkflowExecutionException(WorkflowErrorReason.MODEL_RATE_LIMITED,
|
||||
"raw credentials"), EXECUTE_ID, NODE_ID, "模型分析"));
|
||||
when(states.load(EXECUTE_ID)).thenReturn(state);
|
||||
ChainInfo result = service(executor).getChainStatus(EXECUTE_ID, List.of());
|
||||
Assert.assertTrue(result.getNodes().isEmpty());
|
||||
Assert.assertEquals(NODE_ID, result.getError().getNodeId());
|
||||
Assert.assertEquals("MODEL_RATE_LIMITED", result.getError().getReasonCode());
|
||||
Assert.assertFalse(result.getMessage().contains("credentials"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void terminalWorkflowMustStopAdvertisingPendingRetry() throws Exception {
|
||||
ChainExecutor executor = mock(ChainExecutor.class);
|
||||
ChainStateRepository states = mock(ChainStateRepository.class);
|
||||
NodeStateRepository nodes = mock(NodeStateRepository.class);
|
||||
when(executor.getChainStateRepository()).thenReturn(states);
|
||||
when(executor.getNodeStateRepository()).thenReturn(nodes);
|
||||
NodeState failedAttempt = new NodeState();
|
||||
failedAttempt.setStatus(NodeStatus.ERROR);
|
||||
failedAttempt.setError(new ExceptionSummary(new WorkflowExecutionException(
|
||||
WorkflowErrorReason.MODEL_TIMEOUT, "private cause"), EXECUTE_ID, NODE_ID, "模型分析"));
|
||||
when(nodes.load(EXECUTE_ID, NODE_ID)).thenReturn(failedAttempt);
|
||||
TinyFlowService service = service(executor);
|
||||
for (ChainStatus status : new ChainStatus[]{ChainStatus.RUNNING, ChainStatus.FAILED, ChainStatus.CANCELLED}) {
|
||||
ChainState state = new ChainState();
|
||||
state.setStatus(status);
|
||||
when(states.load(EXECUTE_ID)).thenReturn(state);
|
||||
ChainInfo result = service.getChainStatus(EXECUTE_ID, List.of(node(NodeStatus.READY)));
|
||||
Assert.assertEquals(status == ChainStatus.RUNNING, result.getNodes().get(NODE_ID).getError().isRetryable());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
package tech.easyflow.ai.easyagentsflow.service;
|
||||
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.read.ListAppender;
|
||||
import com.easyagents.flow.core.chain.*;
|
||||
import com.easyagents.flow.core.chain.event.ChainEndEvent;
|
||||
import com.easyagents.flow.core.chain.repository.InMemoryChainStateRepository;
|
||||
import com.easyagents.flow.core.chain.repository.InMemoryNodeStateRepository;
|
||||
import com.easyagents.flow.core.chain.runtime.*;
|
||||
import com.easyagents.flow.core.node.StartNode;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowExecutionError;
|
||||
import tech.easyflow.common.web.error.RequestErrorProfile;
|
||||
import tech.easyflow.common.web.error.WebErrorMapping;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
public class WorkflowFailureDiagnosticsTest {
|
||||
@Test
|
||||
public void singleRunResponseMustMatchLoggedExecutionId() {
|
||||
Logger logger = (Logger) LoggerFactory.getLogger(ChainExecutor.class);
|
||||
ListAppender<ILoggingEvent> logs = new ListAppender<>();
|
||||
logs.start();
|
||||
logger.addAppender(logs);
|
||||
HttpServletRequest request = request();
|
||||
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
WorkflowExecutionErrorMapper.installRequestProfile();
|
||||
RequestErrorProfile profile = (RequestErrorProfile) request.getAttribute(RequestErrorProfile.ATTRIBUTE_NAME);
|
||||
String previousId = null;
|
||||
for (int run = 0; run < 2; run++) {
|
||||
WorkflowExecutionException failure = Assert.assertThrows(WorkflowExecutionException.class,
|
||||
() -> fixture.executor.executeNode("diagnostics", "worker", Map.of("privateInput", "do-not-log")));
|
||||
WebErrorMapping response = profile.map(request, failure);
|
||||
Map<?, ?> data = (Map<?, ?>) response.data();
|
||||
String executeId = (String) data.get("executeId");
|
||||
Assert.assertNotNull(executeId);
|
||||
Assert.assertNotEquals(previousId, executeId);
|
||||
Assert.assertEquals(failure.getChainId(), executeId);
|
||||
Assert.assertEquals(500, response.httpStatus());
|
||||
WorkflowExecutionError error = (WorkflowExecutionError) data.get("error");
|
||||
Assert.assertEquals("MODEL_TIMEOUT", error.getReasonCode());
|
||||
Assert.assertEquals("worker", error.getNodeId());
|
||||
ILoggingEvent event = logs.list.get(run);
|
||||
assertLogContext(event, executeId);
|
||||
Assert.assertTrue(event.getFormattedMessage().contains("attemptKey=" + executeId + ":worker:single"));
|
||||
previousId = executeId;
|
||||
}
|
||||
} finally {
|
||||
RequestContextHolder.resetRequestAttributes();
|
||||
logger.detachAppender(logs);
|
||||
logs.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void optionalExecutionIdMustPreserveExistingRequestMapping() {
|
||||
HttpServletRequest request = request();
|
||||
WebErrorMapping fallback = new WebErrorMapping(400, 400, "原请求错误", null);
|
||||
request.setAttribute(RequestErrorProfile.ATTRIBUTE_NAME, (RequestErrorProfile) (req, failure) -> fallback);
|
||||
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
|
||||
try {
|
||||
WorkflowExecutionErrorMapper.installRequestProfile();
|
||||
RequestErrorProfile profile = (RequestErrorProfile) request.getAttribute(RequestErrorProfile.ATTRIBUTE_NAME);
|
||||
Assert.assertSame(fallback, profile.map(request, new IllegalArgumentException()));
|
||||
WebErrorMapping response = profile.map(request,
|
||||
new WorkflowExecutionException(WorkflowErrorReason.INPUT_INVALID, "internal"));
|
||||
Map<?, ?> data = (Map<?, ?>) response.data();
|
||||
Assert.assertFalse(data.containsKey("executeId"));
|
||||
Assert.assertEquals("INPUT_INVALID", ((WorkflowExecutionError) data.get("error")).getReasonCode());
|
||||
} finally {
|
||||
RequestContextHolder.resetRequestAttributes();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentExecutionsAndRetriesMustHaveDistinctLogContexts() throws Exception {
|
||||
Logger logger = (Logger) LoggerFactory.getLogger(Chain.class);
|
||||
ListAppender<ILoggingEvent> logs = new ListAppender<>();
|
||||
logs.start();
|
||||
logger.addAppender(logs);
|
||||
try (Fixture fixture = new Fixture()) {
|
||||
CountDownLatch ended = new CountDownLatch(2);
|
||||
fixture.executor.addEventListener((event, chain) -> {
|
||||
if (event instanceof ChainEndEvent) ended.countDown();
|
||||
});
|
||||
String first = fixture.executor.executeAsync("diagnostics", Map.of());
|
||||
String second = fixture.executor.executeAsync("diagnostics", Map.of());
|
||||
Assert.assertTrue(ended.await(5, TimeUnit.SECONDS));
|
||||
for (String executeId : List.of(first, second)) {
|
||||
List<ILoggingEvent> attempts = logs.list.stream()
|
||||
.filter(event -> event.getFormattedMessage().contains("executeId=" + executeId + ","))
|
||||
.toList();
|
||||
Assert.assertEquals(2, attempts.size());
|
||||
attempts.forEach(event -> assertLogContext(event, executeId));
|
||||
Assert.assertNotEquals(attempts.get(0).getArgumentArray()[4], attempts.get(1).getArgumentArray()[4]);
|
||||
Assert.assertNotNull(attempts.get(0).getArgumentArray()[4]);
|
||||
}
|
||||
} finally {
|
||||
logger.detachAppender(logs);
|
||||
logs.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private HttpServletRequest request() {
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
when(request.getAttribute(anyString())).thenAnswer(call -> attributes.get(call.getArgument(0)));
|
||||
doAnswer(call -> {
|
||||
attributes.put(call.getArgument(0), call.getArgument(1));
|
||||
return null;
|
||||
}).when(request).setAttribute(anyString(), any());
|
||||
return request;
|
||||
}
|
||||
|
||||
private void assertLogContext(ILoggingEvent event, String executeId) {
|
||||
String message = event.getFormattedMessage();
|
||||
Assert.assertTrue(message.contains("executeId=" + executeId + ","));
|
||||
Assert.assertTrue(message.contains("chainInstanceId=" + executeId + ","));
|
||||
Assert.assertTrue(message.contains("nodeId=worker, nodeName=模型分析,"));
|
||||
Assert.assertFalse(message.contains("do-not-log"));
|
||||
Assert.assertNotNull(event.getThrowableProxy());
|
||||
}
|
||||
|
||||
private static class Fixture implements AutoCloseable {
|
||||
final TriggerScheduler scheduler = new TriggerScheduler(new InMemoryTriggerStore(),
|
||||
Executors.newSingleThreadScheduledExecutor(), Executors.newFixedThreadPool(3), 1000);
|
||||
final ChainExecutor executor;
|
||||
|
||||
Fixture() {
|
||||
ChainDefinition definition = new ChainDefinition();
|
||||
definition.setId("diagnostics");
|
||||
StartNode start = new StartNode(); start.setId("start"); definition.addNode(start);
|
||||
Node worker = new Node() {
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
throw new WorkflowExecutionException(WorkflowErrorReason.MODEL_TIMEOUT, "synthetic cause");
|
||||
}
|
||||
};
|
||||
worker.setId("worker"); worker.setName("模型分析");
|
||||
worker.setRetryEnable(true); worker.setMaxRetryCount(1); worker.setRetryIntervalMs(5);
|
||||
definition.addNode(worker);
|
||||
Edge edge = new Edge(); edge.setId("start-worker"); edge.setSource("start"); edge.setTarget("worker");
|
||||
definition.addEdge(edge);
|
||||
executor = new ChainExecutor(id -> definition, new InMemoryChainStateRepository(),
|
||||
new InMemoryNodeStateRepository(), scheduler);
|
||||
}
|
||||
|
||||
public void close() { scheduler.shutdown(); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user