feat: persist process history events, listeners, and ACTION executions

Give hosts an append-only timeline, post-commit OrdoEventListener hooks, and durable ACTION results without blocking the approval flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
0264408
2026-09-15 10:14:44 +08:00
co-authored by Cursor
parent 77d1e5198b
commit f3220fd0a0
26 changed files with 1063 additions and 67 deletions
@@ -3,17 +3,23 @@ package com.jetlumen.ordo.spring;
import com.jetlumen.ordo.api.ActionHandler;
import com.jetlumen.ordo.api.AssigneeResolver;
import com.jetlumen.ordo.api.OrdoEngine;
import com.jetlumen.ordo.api.OrdoEventListener;
import com.jetlumen.ordo.api.RoutingCondition;
import com.jetlumen.ordo.api.TransactionExecutor;
import com.jetlumen.ordo.api.repository.ActionExecutionRepository;
import com.jetlumen.ordo.api.repository.ApprovalTaskRepository;
import com.jetlumen.ordo.api.repository.ProcessDefinitionRepository;
import com.jetlumen.ordo.api.repository.ProcessHistoryRepository;
import com.jetlumen.ordo.api.repository.ProcessInstanceRepository;
import com.jetlumen.ordo.core.DefaultOrdoEngine;
import com.jetlumen.ordo.storage.jdbc.JdbcActionExecutionRepository;
import com.jetlumen.ordo.storage.jdbc.JdbcApprovalTaskRepository;
import com.jetlumen.ordo.storage.jdbc.JdbcConnectionProvider;
import com.jetlumen.ordo.storage.jdbc.JdbcProcessDefinitionRepository;
import com.jetlumen.ordo.storage.jdbc.JdbcProcessHistoryRepository;
import com.jetlumen.ordo.storage.jdbc.JdbcProcessInstanceRepository;
import com.jetlumen.ordo.storage.jdbc.JdbcTransactionExecutor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@@ -103,6 +109,18 @@ public class OrdoJdbcAutoConfiguration {
return new JdbcApprovalTaskRepository(connectionProvider);
}
@Bean
@ConditionalOnMissingBean
public ProcessHistoryRepository ordoProcessHistoryRepository(JdbcConnectionProvider connectionProvider) {
return new JdbcProcessHistoryRepository(connectionProvider);
}
@Bean
@ConditionalOnMissingBean
public ActionExecutionRepository ordoActionExecutionRepository(JdbcConnectionProvider connectionProvider) {
return new JdbcActionExecutionRepository(connectionProvider);
}
@Bean
@ConditionalOnMissingBean
public OrdoEngine ordoEngine(Clock ordoClock,
@@ -112,10 +130,14 @@ public class OrdoJdbcAutoConfiguration {
TransactionExecutor ordoTransactionExecutor,
ProcessDefinitionRepository ordoProcessDefinitionRepository,
ProcessInstanceRepository ordoProcessInstanceRepository,
ApprovalTaskRepository ordoApprovalTaskRepository) {
ApprovalTaskRepository ordoApprovalTaskRepository,
ProcessHistoryRepository ordoProcessHistoryRepository,
ActionExecutionRepository ordoActionExecutionRepository,
ObjectProvider<OrdoEventListener> ordoEventListeners) {
return new DefaultOrdoEngine(ordoClock, ordoAssigneeResolver, ordoRoutingCondition, actionHandler,
ordoTransactionExecutor, ordoProcessDefinitionRepository, ordoProcessInstanceRepository,
ordoApprovalTaskRepository);
ordoApprovalTaskRepository, ordoProcessHistoryRepository, ordoActionExecutionRepository,
ordoEventListeners.orderedStream().toList());
}
@Bean
@@ -5,9 +5,13 @@ import com.jetlumen.ordo.api.ApprovalStep;
import com.jetlumen.ordo.api.ApprovalTask;
import com.jetlumen.ordo.api.AssigneeResolver;
import com.jetlumen.ordo.api.OrdoEngine;
import com.jetlumen.ordo.api.OrdoEventListener;
import com.jetlumen.ordo.api.ProcessDefinition;
import com.jetlumen.ordo.api.ProcessEvent;
import com.jetlumen.ordo.api.ProcessEventType;
import com.jetlumen.ordo.api.ProcessInstance;
import com.jetlumen.ordo.api.RoutingCondition;
import com.jetlumen.ordo.api.query.PageRequest;
import com.jetlumen.ordo.api.repository.ProcessDefinitionRepository;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
@@ -151,6 +155,27 @@ class OrdoJdbcAutoConfigurationTest {
});
}
@Test
void invokesOrdoEventListenerBeans() {
withDataSourceRunner.withUserConfiguration(RecordingListenerConfig.class)
.run(context -> {
OrdoEngine engine = context.getBean(OrdoEngine.class);
engine.register(LEAVE_REQUEST);
ProcessInstance instance = engine.start("leave-request", "alice");
engine.approve(engine.findPendingTasksByAssignee("maria").getFirst().id(), "maria");
RecordingListener listener = context.getBean(RecordingListener.class);
assertThat(listener.types).contains(
ProcessEventType.INSTANCE_STARTED,
ProcessEventType.TASK_CREATED,
ProcessEventType.TASK_APPROVED,
ProcessEventType.INSTANCE_APPROVED);
assertThat(listener.types.getFirst()).isEqualTo(ProcessEventType.INSTANCE_STARTED);
assertThat(engine.queryHistory(instance.id(), new PageRequest(0, 20))
.totalElements()).isGreaterThan(0);
});
}
@Configuration
static class CustomAssigneeResolverConfig {
@Bean
@@ -176,4 +201,21 @@ class OrdoJdbcAutoConfigurationTest {
};
}
}
static class RecordingListener implements OrdoEventListener {
final List<ProcessEventType> types = new java.util.concurrent.CopyOnWriteArrayList<>();
@Override
public void onEvent(ProcessEvent event) {
types.add(event.type());
}
}
@Configuration
static class RecordingListenerConfig {
@Bean
RecordingListener recordingListener() {
return new RecordingListener();
}
}
}