fix: 修复工作流同步执行并发监听异常

- 使用写时复制集合保证事件分发期间安全增删监听器

- 按工作流实例路由同步结果并补充并发回归测试
This commit is contained in:
2026-07-23 11:52:50 +08:00
parent f057900f7a
commit 7e59f0e638
4 changed files with 278 additions and 27 deletions

View File

@@ -20,23 +20,31 @@ import com.easyagents.flow.core.chain.listener.*;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.*; import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 管理工作流执行过程中的事件、输出及错误监听器。
*
* <p>监听器的注册与移除频率远低于事件分发频率,使用写时复制集合保证分发过程可以无锁遍历,
* 同时允许其他线程安全地注册或移除监听器。</p>
*/
public class EventManager { public class EventManager {
private static final Logger log = LoggerFactory.getLogger(EventManager.class); private static final Logger log = LoggerFactory.getLogger(EventManager.class);
protected final Map<Class<?>, List<ChainEventListener>> eventListeners = new ConcurrentHashMap<>(); protected final Map<Class<?>, List<ChainEventListener>> eventListeners = new ConcurrentHashMap<>();
protected final List<ChainOutputListener> outputListeners = Collections.synchronizedList(new ArrayList<>()); protected final List<ChainOutputListener> outputListeners = new CopyOnWriteArrayList<>();
protected final List<ChainErrorListener> chainErrorListeners = Collections.synchronizedList(new ArrayList<>()); protected final List<ChainErrorListener> chainErrorListeners = new CopyOnWriteArrayList<>();
protected final List<NodeErrorListener> nodeErrorListeners = Collections.synchronizedList(new ArrayList<>()); protected final List<NodeErrorListener> nodeErrorListeners = new CopyOnWriteArrayList<>();
/** /**
* ---------- 通用事件监听器 ---------- * ---------- 通用事件监听器 ----------
*/ */
public void addEventListener(Class<? extends Event> eventClass, ChainEventListener listener) { public void addEventListener(Class<? extends Event> eventClass, ChainEventListener listener) {
eventListeners.computeIfAbsent(eventClass, k -> Collections.synchronizedList(new ArrayList<>())).add(listener); eventListeners.computeIfAbsent(eventClass, key -> new CopyOnWriteArrayList<>()).add(listener);
} }
public void addEventListener(ChainEventListener listener) { public void addEventListener(ChainEventListener listener) {

View File

@@ -44,6 +44,9 @@ public class ChainExecutor {
private final NodeStateRepository nodeStateRepository; private final NodeStateRepository nodeStateRepository;
private final TriggerScheduler triggerScheduler; private final TriggerScheduler triggerScheduler;
private final EventManager eventManager = new EventManager(); private final EventManager eventManager = new EventManager();
/** 等待同步执行结果的任务,按工作流实例 ID 进行常量时间路由。 */
private final ConcurrentMap<String, CompletableFuture<Map<String, Object>>> pendingExecutions =
new ConcurrentHashMap<>();
public ChainExecutor(ChainDefinitionRepository definitionRepository public ChainExecutor(ChainDefinitionRepository definitionRepository
, ChainStateRepository chainStateRepository , ChainStateRepository chainStateRepository
@@ -53,7 +56,7 @@ public class ChainExecutor {
this.chainStateRepository = chainStateRepository; this.chainStateRepository = chainStateRepository;
this.nodeStateRepository = nodeStateRepository; this.nodeStateRepository = nodeStateRepository;
this.triggerScheduler = ChainRuntime.triggerScheduler(); this.triggerScheduler = ChainRuntime.triggerScheduler();
this.triggerScheduler.registerConsumer(this::accept); registerRuntimeCallbacks();
} }
@@ -65,7 +68,7 @@ public class ChainExecutor {
this.chainStateRepository = chainStateRepository; this.chainStateRepository = chainStateRepository;
this.nodeStateRepository = nodeStateRepository; this.nodeStateRepository = nodeStateRepository;
this.triggerScheduler = triggerScheduler; this.triggerScheduler = triggerScheduler;
this.triggerScheduler.registerConsumer(this::accept); registerRuntimeCallbacks();
} }
@@ -79,26 +82,12 @@ public class ChainExecutor {
String stateInstanceId = chain.getStateInstanceId(); String stateInstanceId = chain.getStateInstanceId();
CompletableFuture<Map<String, Object>> future = new CompletableFuture<>(); CompletableFuture<Map<String, Object>> future = new CompletableFuture<>();
ChainEventListener listener = (event, c) -> { CompletableFuture<Map<String, Object>> existing = pendingExecutions.putIfAbsent(stateInstanceId, future);
if (event instanceof ChainStatusChangeEvent) { if (existing != null) {
if (((ChainStatusChangeEvent) event).getStatus().isTerminal() throw new IllegalStateException("Duplicate pending chain execution: " + stateInstanceId);
&& c.getStateInstanceId().equals(stateInstanceId)) { }
ChainState state = chainStateRepository.load(stateInstanceId);
Map<String, Object> execResult = state.getExecuteResult();
future.complete(execResult != null ? execResult : Collections.emptyMap());
}
}
};
ChainErrorListener errorListener = (error, c) -> {
if (c.getStateInstanceId().equals(stateInstanceId)) {
future.completeExceptionally(error);
}
};
try { try {
this.addEventListener(listener);
this.addErrorListener(errorListener);
chain.start(variables); chain.start(variables);
Map<String, Object> result = future.get(timeout, unit); Map<String, Object> result = future.get(timeout, unit);
clearDefaultStates(result); clearDefaultStates(result);
@@ -114,8 +103,59 @@ public class ChainExecutor {
future.cancel(true); future.cancel(true);
throw new RuntimeException("Execution failed", e.getCause()); throw new RuntimeException("Execution failed", e.getCause());
} finally { } finally {
this.removeEventListener(listener); pendingExecutions.remove(stateInstanceId, future);
this.removeErrorListener(errorListener); }
}
/**
* 注册工作流调度和同步结果路由回调。
*/
private void registerRuntimeCallbacks() {
eventManager.addEventListener(ChainStatusChangeEvent.class, this::completePendingExecution);
eventManager.addChainErrorListener(this::failPendingExecution);
triggerScheduler.registerConsumer(this::accept);
}
/**
* 在工作流进入终态时完成对应的同步等待任务。
*
* @param event 工作流状态事件
* @param chain 产生事件的工作流实例
*/
private void completePendingExecution(Event event, Chain chain) {
if (!(event instanceof ChainStatusChangeEvent statusChangeEvent)
|| !statusChangeEvent.getStatus().isTerminal()) {
return;
}
String stateInstanceId = chain.getStateInstanceId();
CompletableFuture<Map<String, Object>> future = pendingExecutions.get(stateInstanceId);
if (future == null) {
return;
}
try {
ChainState state = chainStateRepository.load(stateInstanceId);
if (state == null) {
throw new ChainException("Chain state not found: " + stateInstanceId);
}
Map<String, Object> execResult = state.getExecuteResult();
future.complete(execResult != null ? execResult : Collections.emptyMap());
} catch (Exception error) {
future.completeExceptionally(error);
}
}
/**
* 将工作流执行异常传递给对应的同步等待任务。
*
* @param error 工作流执行异常
* @param chain 发生异常的工作流实例
*/
private void failPendingExecution(Throwable error, Chain chain) {
CompletableFuture<Map<String, Object>> future = pendingExecutions.get(chain.getStateInstanceId());
if (future != null) {
future.completeExceptionally(error);
} }
} }

View File

@@ -0,0 +1,112 @@
/**
* 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.test;
import com.easyagents.flow.core.chain.ChainDefinition;
import com.easyagents.flow.core.chain.Edge;
import com.easyagents.flow.core.chain.repository.InMemoryChainStateRepository;
import com.easyagents.flow.core.chain.repository.InMemoryNodeStateRepository;
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
import com.easyagents.flow.core.chain.runtime.InMemoryTriggerStore;
import com.easyagents.flow.core.chain.runtime.TriggerScheduler;
import com.easyagents.flow.core.node.EndNode;
import com.easyagents.flow.core.node.StartNode;
import org.junit.Assert;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* {@link ChainExecutor} 并发同步执行测试。
*/
public class ChainExecutorConcurrencyTest {
/**
* 验证多个同步调用可以通过实例 ID 独立接收执行结果。
*
* @throws Exception 并发任务执行失败、超时或中断时抛出
*/
@Test
public void shouldRouteConcurrentSynchronousExecutionResults() throws Exception {
ScheduledExecutorService schedulerPool = Executors.newScheduledThreadPool(2);
ExecutorService workerPool = Executors.newFixedThreadPool(8);
ExecutorService callerPool = Executors.newFixedThreadPool(8);
TriggerScheduler triggerScheduler = new TriggerScheduler(
new InMemoryTriggerStore(), schedulerPool, workerPool, 1000L);
ChainDefinition definition = createDefinition();
ChainExecutor chainExecutor = new ChainExecutor(
id -> definition,
new InMemoryChainStateRepository(),
new InMemoryNodeStateRepository(),
triggerScheduler);
int executionCount = 16;
CountDownLatch startGate = new CountDownLatch(1);
List<Future<Map<String, Object>>> executions = new ArrayList<>(executionCount);
try {
for (int index = 0; index < executionCount; index++) {
executions.add(callerPool.submit(() -> {
startGate.await();
return chainExecutor.execute(definition.getId(), Collections.emptyMap());
}));
}
startGate.countDown();
for (Future<Map<String, Object>> execution : executions) {
Assert.assertNotNull(execution.get(10, TimeUnit.SECONDS));
}
} finally {
startGate.countDown();
callerPool.shutdownNow();
triggerScheduler.shutdown();
}
}
/**
* 创建仅包含开始和结束节点的测试工作流。
*
* @return 测试工作流定义
*/
private ChainDefinition createDefinition() {
ChainDefinition definition = new ChainDefinition();
definition.setId("concurrent-sync-test");
StartNode startNode = new StartNode();
startNode.setId("start");
EndNode endNode = new EndNode();
endNode.setId("end");
Edge edge = new Edge();
edge.setId("start-to-end");
edge.setSource(startNode.getId());
edge.setTarget(endNode.getId());
definition.addNode(startNode);
definition.addNode(endNode);
definition.addEdge(edge);
return definition;
}
}

View File

@@ -0,0 +1,91 @@
/**
* 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.test;
import com.easyagents.flow.core.chain.EventManager;
import com.easyagents.flow.core.chain.event.ChainEndEvent;
import com.easyagents.flow.core.chain.listener.ChainEventListener;
import org.junit.Assert;
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
* {@link EventManager} 并发行为测试。
*/
public class EventManagerConcurrencyTest {
/**
* 验证事件分发期间可以从其他线程移除监听器,当前分发使用稳定快照,后续分发不再调用已移除监听器。
*
* @throws Exception 等待测试线程超时或中断时抛出
*/
@Test
public void shouldAllowListenerRemovalDuringEventDispatch() throws Exception {
EventManager eventManager = new EventManager();
CountDownLatch dispatchStarted = new CountDownLatch(1);
CountDownLatch continueDispatch = new CountDownLatch(1);
AtomicInteger retainedInvocationCount = new AtomicInteger();
AtomicInteger removedInvocationCount = new AtomicInteger();
AtomicReference<Throwable> dispatchFailure = new AtomicReference<>();
ChainEventListener blockingListener = (event, chain) -> {
dispatchStarted.countDown();
try {
if (!continueDispatch.await(3, TimeUnit.SECONDS)) {
throw new AssertionError("事件分发等待超时");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new AssertionError("事件分发线程被中断", e);
}
};
ChainEventListener retainedListener = (event, chain) -> retainedInvocationCount.incrementAndGet();
ChainEventListener removedListener = (event, chain) -> removedInvocationCount.incrementAndGet();
eventManager.addEventListener(blockingListener);
eventManager.addEventListener(retainedListener);
eventManager.addEventListener(removedListener);
Thread dispatchThread = new Thread(() -> {
try {
eventManager.notifyEvent(new ChainEndEvent(null), null);
} catch (Throwable error) {
dispatchFailure.set(error);
}
}, "event-manager-dispatch-test");
dispatchThread.start();
try {
Assert.assertTrue("事件分发应在测试超时前开始", dispatchStarted.await(3, TimeUnit.SECONDS));
eventManager.removeEventListener(removedListener);
} finally {
continueDispatch.countDown();
}
dispatchThread.join(3000L);
Assert.assertFalse("事件分发线程应正常结束", dispatchThread.isAlive());
Assert.assertNull("并发移除监听器不应中断事件分发", dispatchFailure.get());
Assert.assertEquals(1, retainedInvocationCount.get());
Assert.assertEquals("当前事件应按分发开始时的快照通知", 1, removedInvocationCount.get());
eventManager.notifyEvent(new ChainEndEvent(null), null);
Assert.assertEquals(2, retainedInvocationCount.get());
Assert.assertEquals("后续事件不应通知已移除监听器", 1, removedInvocationCount.get());
}
}