feat: add structured PARALLEL blocks with join-all tokens

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
0264408
2026-09-18 18:17:53 +08:00
co-authored by Cursor
parent 0e9bb4bd16
commit e4478656c1
31 changed files with 1300 additions and 62 deletions
@@ -0,0 +1,164 @@
package com.jetlumen.ordo.storage.jdbc;
import com.jetlumen.ordo.api.InstanceToken;
import com.jetlumen.ordo.api.TokenStatus;
import com.jetlumen.ordo.api.repository.InstanceTokenRepository;
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialect;
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialects;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/** JDBC implementation of the parallel token port. */
public final class JdbcInstanceTokenRepository implements InstanceTokenRepository {
private static final String COLUMNS =
"id, instance_id, parallel_step_id, branch_id, current_step_id, status";
private static final String INSERT =
"INSERT INTO ordo_instance_token (id, instance_id, parallel_step_id, branch_id, current_step_id, status)"
+ " VALUES (?, ?, ?, ?, ?, ?)";
private static final String SELECT_BY_INSTANCE =
"SELECT " + COLUMNS + " FROM ordo_instance_token WHERE instance_id = ? ORDER BY parallel_step_id, branch_id";
private static final String SELECT_BY_PARALLEL =
"SELECT " + COLUMNS + " FROM ordo_instance_token WHERE instance_id = ? AND parallel_step_id = ?"
+ " ORDER BY branch_id";
private static final String COMPLETE_IF_ACTIVE =
"UPDATE ordo_instance_token SET status = 'COMPLETED' WHERE instance_id = ? AND parallel_step_id = ?"
+ " AND branch_id = ? AND status = 'ACTIVE'";
private static final String UPDATE_CURRENT_IF_ACTIVE =
"UPDATE ordo_instance_token SET current_step_id = ? WHERE instance_id = ? AND parallel_step_id = ?"
+ " AND branch_id = ? AND status = 'ACTIVE'";
private static final String COMPLETE_ACTIVE_BY_INSTANCE =
"UPDATE ordo_instance_token SET status = 'COMPLETED' WHERE instance_id = ? AND status = 'ACTIVE'";
private final JdbcConnectionProvider connectionProvider;
public JdbcInstanceTokenRepository(JdbcConnectionProvider connectionProvider) {
this(connectionProvider, SqlDialects.postgresql());
}
public JdbcInstanceTokenRepository(JdbcConnectionProvider connectionProvider, SqlDialect dialect) {
this.connectionProvider = Objects.requireNonNull(connectionProvider, "connectionProvider must not be null");
Objects.requireNonNull(dialect, "dialect must not be null");
}
@Override
public void insert(InstanceToken token) {
Objects.requireNonNull(token, "token must not be null");
Connection connection = connectionProvider.getConnection();
try (PreparedStatement insert = connection.prepareStatement(INSERT)) {
insert.setString(1, token.id());
insert.setString(2, token.instanceId());
insert.setString(3, token.parallelStepId());
insert.setString(4, token.branchId());
insert.setString(5, token.currentStepId());
insert.setString(6, token.status().name());
insert.executeUpdate();
} catch (SQLException e) {
throw new JdbcStorageException("failed to insert token: " + token.id(), e);
} finally {
connectionProvider.close(connection);
}
}
@Override
public List<InstanceToken> findByInstanceId(String instanceId) {
Objects.requireNonNull(instanceId, "instanceId must not be null");
return query(SELECT_BY_INSTANCE, instanceId);
}
@Override
public List<InstanceToken> findByInstanceAndParallel(String instanceId, String parallelStepId) {
Objects.requireNonNull(instanceId, "instanceId must not be null");
Objects.requireNonNull(parallelStepId, "parallelStepId must not be null");
Connection connection = connectionProvider.getConnection();
try (PreparedStatement select = connection.prepareStatement(SELECT_BY_PARALLEL)) {
select.setString(1, instanceId);
select.setString(2, parallelStepId);
return readAll(select);
} catch (SQLException e) {
throw new JdbcStorageException("failed to load tokens for parallel: " + parallelStepId, e);
} finally {
connectionProvider.close(connection);
}
}
@Override
public boolean completeIfActive(String instanceId, String parallelStepId, String branchId) {
return update(COMPLETE_IF_ACTIVE, instanceId, parallelStepId, branchId);
}
@Override
public boolean updateCurrentStepIfActive(String instanceId, String parallelStepId, String branchId,
String currentStepId) {
Objects.requireNonNull(currentStepId, "currentStepId must not be null");
Connection connection = connectionProvider.getConnection();
try (PreparedStatement update = connection.prepareStatement(UPDATE_CURRENT_IF_ACTIVE)) {
update.setString(1, currentStepId);
update.setString(2, instanceId);
update.setString(3, parallelStepId);
update.setString(4, branchId);
return update.executeUpdate() == 1;
} catch (SQLException e) {
throw new JdbcStorageException("failed to update token current step", e);
} finally {
connectionProvider.close(connection);
}
}
@Override
public void completeActiveByInstanceId(String instanceId) {
Objects.requireNonNull(instanceId, "instanceId must not be null");
Connection connection = connectionProvider.getConnection();
try (PreparedStatement update = connection.prepareStatement(COMPLETE_ACTIVE_BY_INSTANCE)) {
update.setString(1, instanceId);
update.executeUpdate();
} catch (SQLException e) {
throw new JdbcStorageException("failed to complete tokens for instance: " + instanceId, e);
} finally {
connectionProvider.close(connection);
}
}
private List<InstanceToken> query(String sql, String instanceId) {
Connection connection = connectionProvider.getConnection();
try (PreparedStatement select = connection.prepareStatement(sql)) {
select.setString(1, instanceId);
return readAll(select);
} catch (SQLException e) {
throw new JdbcStorageException("failed to load tokens for instance: " + instanceId, e);
} finally {
connectionProvider.close(connection);
}
}
private boolean update(String sql, String instanceId, String parallelStepId, String branchId) {
Connection connection = connectionProvider.getConnection();
try (PreparedStatement update = connection.prepareStatement(sql)) {
update.setString(1, instanceId);
update.setString(2, parallelStepId);
update.setString(3, branchId);
return update.executeUpdate() == 1;
} catch (SQLException e) {
throw new JdbcStorageException("failed to update token", e);
} finally {
connectionProvider.close(connection);
}
}
private static List<InstanceToken> readAll(PreparedStatement select) throws SQLException {
List<InstanceToken> tokens = new ArrayList<>();
try (ResultSet resultSet = select.executeQuery()) {
while (resultSet.next()) {
tokens.add(new InstanceToken(resultSet.getString("id"), resultSet.getString("instance_id"),
resultSet.getString("parallel_step_id"), resultSet.getString("branch_id"),
resultSet.getString("current_step_id"), TokenStatus.valueOf(resultSet.getString("status"))));
}
}
return tokens;
}
}
@@ -1,7 +1,9 @@
package com.jetlumen.ordo.storage.jdbc;
import com.jetlumen.ordo.api.ApprovalStep;
import com.jetlumen.ordo.api.ParallelBranch;
import com.jetlumen.ordo.api.ProcessDefinition;
import com.jetlumen.ordo.api.StepKind;
import com.jetlumen.ordo.api.StepTransition;
import com.jetlumen.ordo.api.query.Page;
import com.jetlumen.ordo.api.query.PageRequest;
@@ -19,8 +21,10 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -39,8 +43,8 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
"INSERT INTO ordo_process_definition (id, version, name, created_at) VALUES (?, ?, ?, ?)";
private static final String INSERT_STEP =
"INSERT INTO ordo_approval_step (definition_id, definition_version, step_id, step_name, policy, step_order,"
+ " kind, action_key, due_after, due_then, due_to, due_action)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+ " kind, action_key, due_after, due_then, due_to, due_action, parent_step_id, branch_id, branch_order)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
private static final String INSERT_CANDIDATE =
"INSERT INTO ordo_step_candidate (definition_id, definition_version, step_id, candidate, candidate_order)"
+ " VALUES (?, ?, ?, ?, ?)";
@@ -50,7 +54,8 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
private static final String SELECT_DEFINITION =
"SELECT name FROM ordo_process_definition WHERE id = ? AND version = ?";
private static final String SELECT_STEPS =
"SELECT step_id, step_name, policy, kind, action_key, due_after, due_then, due_to, due_action"
"SELECT step_id, step_name, policy, kind, action_key, due_after, due_then, due_to, due_action,"
+ " parent_step_id, branch_id, branch_order"
+ " FROM ordo_approval_step"
+ " WHERE definition_id = ? AND definition_version = ? ORDER BY step_order";
private static final String SELECT_CANDIDATES =
@@ -248,12 +253,7 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
}
}
}
List<ApprovalStep> steps = new ArrayList<>();
for (StepRow row : stepRows) {
List<String> candidates = candidatesByStep.getOrDefault(row.stepId(), List.of());
steps.add(new ApprovalStep(row.stepId(), row.stepName(), candidates, row.policy(), row.kind(),
row.actionKey(), ApprovalStepMapper.toDue(row)));
}
List<ApprovalStep> steps = assembleSteps(stepRows, candidatesByStep);
List<StepTransition> transitions = new ArrayList<>();
try (PreparedStatement selectTransitions = connection.prepareStatement(SELECT_TRANSITIONS)) {
selectTransitions.setString(1, definitionId);
@@ -269,6 +269,41 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
return new ProcessDefinition(definitionId, version, name, steps, transitions);
}
private static List<ApprovalStep> assembleSteps(List<StepRow> stepRows, Map<String, List<String>> candidatesByStep) {
Map<String, LinkedHashMap<String, List<String>>> branchesByParent = new LinkedHashMap<>();
Map<String, Integer> branchOrderByKey = new LinkedHashMap<>();
for (StepRow row : stepRows) {
if (row.parentStepId() == null || row.parentStepId().isBlank()) {
continue;
}
branchOrderByKey.putIfAbsent(row.parentStepId() + '\0' + row.branchId(),
row.branchOrder() == null ? Integer.MAX_VALUE : row.branchOrder());
branchesByParent.computeIfAbsent(row.parentStepId(), key -> new LinkedHashMap<>())
.computeIfAbsent(row.branchId(), key -> new ArrayList<>())
.add(row.stepId());
}
List<ApprovalStep> steps = new ArrayList<>();
for (StepRow row : stepRows) {
List<String> candidates = candidatesByStep.getOrDefault(row.stepId(), List.of());
List<ParallelBranch> branches = List.of();
if (row.kind() == StepKind.PARALLEL) {
LinkedHashMap<String, List<String>> byBranch = branchesByParent.getOrDefault(row.stepId(),
new LinkedHashMap<>());
List<Map.Entry<String, List<String>>> ordered = new ArrayList<>(byBranch.entrySet());
ordered.sort(Comparator.comparingInt(entry ->
branchOrderByKey.getOrDefault(row.stepId() + '\0' + entry.getKey(), Integer.MAX_VALUE)));
List<ParallelBranch> assembled = new ArrayList<>();
for (Map.Entry<String, List<String>> entry : ordered) {
assembled.add(new ParallelBranch(entry.getKey(), entry.getValue()));
}
branches = assembled;
}
steps.add(new ApprovalStep(row.stepId(), row.stepName(), candidates, row.policy(), row.kind(),
row.actionKey(), ApprovalStepMapper.toDue(row), branches));
}
return steps;
}
private Integer lockCurrentVersion(Connection connection, String definitionId) throws SQLException {
try (PreparedStatement select = connection.prepareStatement(lockProcess)) {
select.setString(1, definitionId);
@@ -336,6 +371,23 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
}
private static void insertGraph(Connection connection, ProcessDefinition definition) throws SQLException {
Map<String, String> parentByStep = new LinkedHashMap<>();
Map<String, String> branchByStep = new LinkedHashMap<>();
Map<String, Integer> branchOrderByStep = new LinkedHashMap<>();
for (ApprovalStep step : definition.steps()) {
if (step.kind() != StepKind.PARALLEL) {
continue;
}
int branchOrder = 0;
for (ParallelBranch branch : step.branches()) {
for (String nestedId : branch.stepIds()) {
parentByStep.put(nestedId, step.id());
branchByStep.put(nestedId, branch.id());
branchOrderByStep.put(nestedId, branchOrder);
}
branchOrder++;
}
}
int stepOrder = 0;
for (ApprovalStep step : definition.steps()) {
try (PreparedStatement insertStep = connection.prepareStatement(INSERT_STEP)) {
@@ -358,6 +410,14 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
SqlBindings.setString(insertStep, 11, step.due().to());
SqlBindings.setString(insertStep, 12, step.due().action());
}
SqlBindings.setString(insertStep, 13, parentByStep.get(step.id()));
SqlBindings.setString(insertStep, 14, branchByStep.get(step.id()));
Integer branchOrder = branchOrderByStep.get(step.id());
if (branchOrder == null) {
insertStep.setNull(15, Types.INTEGER);
} else {
insertStep.setInt(15, branchOrder);
}
insertStep.executeUpdate();
}
int candidateOrder = 0;
@@ -26,7 +26,15 @@ public final class ApprovalStepMapper {
resultSet.getString("due_after"),
resultSet.getString("due_then"),
resultSet.getString("due_to"),
resultSet.getString("due_action"));
resultSet.getString("due_action"),
resultSet.getString("parent_step_id"),
resultSet.getString("branch_id"),
intOrNull(resultSet, "branch_order"));
}
private static Integer intOrNull(ResultSet resultSet, String column) throws SQLException {
int value = resultSet.getInt(column);
return resultSet.wasNull() ? null : value;
}
public static StepDue toDue(StepRow row) {
@@ -38,6 +46,7 @@ public final class ApprovalStepMapper {
}
public record StepRow(String stepId, String stepName, ApprovalPolicy policy, StepKind kind, String actionKey,
String dueAfter, String dueThen, String dueTo, String dueAction) {
String dueAfter, String dueThen, String dueTo, String dueAction, String parentStepId,
String branchId, Integer branchOrder) {
}
}
@@ -0,0 +1,20 @@
-- Structured PARALLEL blocks: parent/branch on steps, runtime tokens.
ALTER TABLE ordo_approval_step ADD COLUMN parent_step_id VARCHAR(64);
ALTER TABLE ordo_approval_step ADD COLUMN branch_id VARCHAR(64);
ALTER TABLE ordo_approval_step ADD COLUMN branch_order INTEGER;
CREATE TABLE ordo_instance_token (
id VARCHAR(36) NOT NULL,
instance_id VARCHAR(36) NOT NULL,
parallel_step_id VARCHAR(64) NOT NULL,
branch_id VARCHAR(64) NOT NULL,
current_step_id VARCHAR(64) NOT NULL,
status VARCHAR(16) NOT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_instance_token_instance FOREIGN KEY (instance_id) REFERENCES ordo_process_instance (id),
CONSTRAINT uq_instance_token_branch UNIQUE (instance_id, parallel_step_id, branch_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE INDEX idx_instance_token_instance ON ordo_instance_token (instance_id);
CREATE INDEX idx_instance_token_parallel ON ordo_instance_token (instance_id, parallel_step_id);
@@ -0,0 +1,19 @@
-- Structured PARALLEL blocks: parent/branch on steps, runtime tokens.
ALTER TABLE ordo_approval_step ADD COLUMN parent_step_id VARCHAR(64);
ALTER TABLE ordo_approval_step ADD COLUMN branch_id VARCHAR(64);
ALTER TABLE ordo_approval_step ADD COLUMN branch_order INTEGER;
CREATE TABLE ordo_instance_token (
id VARCHAR(36) PRIMARY KEY,
instance_id VARCHAR(36) NOT NULL,
parallel_step_id VARCHAR(64) NOT NULL,
branch_id VARCHAR(64) NOT NULL,
current_step_id VARCHAR(64) NOT NULL,
status VARCHAR(16) NOT NULL,
CONSTRAINT fk_instance_token_instance FOREIGN KEY (instance_id) REFERENCES ordo_process_instance (id),
CONSTRAINT uq_instance_token_branch UNIQUE (instance_id, parallel_step_id, branch_id)
);
CREATE INDEX idx_instance_token_instance ON ordo_instance_token (instance_id);
CREATE INDEX idx_instance_token_parallel ON ordo_instance_token (instance_id, parallel_step_id);
@@ -5,11 +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.ParallelBranch;
import com.jetlumen.ordo.api.ProcessContext;
import com.jetlumen.ordo.api.ProcessDefinition;
import com.jetlumen.ordo.api.ProcessInstance;
import com.jetlumen.ordo.api.ProcessStatus;
import com.jetlumen.ordo.api.RoutingCondition;
import com.jetlumen.ordo.api.StepTransition;
import com.jetlumen.ordo.api.TaskAction;
import com.jetlumen.ordo.api.TaskStatus;
import com.jetlumen.ordo.core.DefaultOrdoEngine;
@@ -76,7 +78,8 @@ class JdbcMysqlIntegrationTest {
}
MysqlDataSource schemaDataSource = newDataSource(DATABASE);
JdbcTestSupport.applySchema(schemaDataSource, JdbcTestSupport.MYSQL_BASELINE, JdbcTestSupport.MYSQL_V2);
JdbcTestSupport.applySchema(schemaDataSource, JdbcTestSupport.MYSQL_BASELINE, JdbcTestSupport.MYSQL_V2,
JdbcTestSupport.MYSQL_V3);
dataSource = schemaDataSource;
}
@@ -118,6 +121,19 @@ class JdbcMysqlIntegrationTest {
assertEquals("LEAVE-2026-001", finished.context().value("requestId").orElseThrow());
}
@Test
void engineJoinsAParallelBlock() {
OrdoEngine engine = newEngine(AssigneeResolver.direct());
engine.publish(parallelReview("parallel-mysql"));
ProcessInstance instance = engine.start("parallel-mysql", "alice");
assertEquals(2, engine.findPendingTasksByInstanceId(instance.id()).size());
for (ApprovalTask task : engine.findPendingTasksByInstanceId(instance.id())) {
engine.approve(task.id(), task.assignee());
}
engine.approve(engine.findPendingTasksByInstanceId(instance.id()).get(0).id(), "cara");
assertEquals(ProcessStatus.APPROVED, engine.findInstance(instance.id()).orElseThrow().status());
}
@Test
void publishIsIdempotentForTheSameGraphAndVersionsAChangedGraph() {
JdbcProcessDefinitionRepository repository =
@@ -233,9 +249,25 @@ class JdbcMysqlIntegrationTest {
new JdbcApprovalTaskRepository(connectionProvider, DIALECT),
new JdbcProcessHistoryRepository(connectionProvider, DIALECT),
new JdbcActionExecutionRepository(connectionProvider, DIALECT),
new JdbcInstanceTokenRepository(connectionProvider, DIALECT),
List.of());
}
private static ProcessDefinition parallelReview(String id) {
return new ProcessDefinition(id, "P", List.of(
ApprovalStep.parallel("dept", "Dept", List.of(
new ParallelBranch("legal", List.of("legal")),
new ParallelBranch("finance", List.of("finance")))),
ApprovalStep.single("legal", "Legal", "lee"),
ApprovalStep.single("finance", "Finance", "fay"),
ApprovalStep.single("ceo", "CEO", "cara")),
List.of(
StepTransition.end("legal"),
StepTransition.end("finance"),
StepTransition.always("dept", "ceo"),
StepTransition.end("ceo")));
}
private static MysqlDataSource newDataSource(String database) {
MysqlDataSource mysqlDataSource = new MysqlDataSource();
String host = JdbcTestSupport.config("ordo.test.mysql.host", "ORDO_TEST_MYSQL_HOST", "localhost");
@@ -7,6 +7,7 @@ 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.ParallelBranch;
import com.jetlumen.ordo.api.ProcessContext;
import com.jetlumen.ordo.api.ProcessDefinition;
import com.jetlumen.ordo.api.ProcessEvent;
@@ -134,6 +135,7 @@ class JdbcOrdoEngineIntegrationTest {
new JdbcApprovalTaskRepository(connectionProvider),
new JdbcProcessHistoryRepository(connectionProvider),
new JdbcActionExecutionRepository(connectionProvider),
new JdbcInstanceTokenRepository(connectionProvider),
List.of());
startEngine.publish(dueDefinition);
ProcessInstance instance = startEngine.start("leave-due", "alice");
@@ -147,6 +149,7 @@ class JdbcOrdoEngineIntegrationTest {
new JdbcApprovalTaskRepository(connectionProvider),
new JdbcProcessHistoryRepository(connectionProvider),
new JdbcActionExecutionRepository(connectionProvider),
new JdbcInstanceTokenRepository(connectionProvider),
List.of());
assertEquals(1, laterEngine.processDue(10));
ApprovalTask escalated = laterEngine.findPendingTasksByInstanceId(instance.id()).get(0);
@@ -190,6 +193,7 @@ class JdbcOrdoEngineIntegrationTest {
new JdbcApprovalTaskRepository(connectionProvider),
new JdbcProcessHistoryRepository(connectionProvider),
new JdbcActionExecutionRepository(connectionProvider),
new JdbcInstanceTokenRepository(connectionProvider),
List.of());
failingEngine.publish(new ProcessDefinition("leave-noroute", "Leave request",
List.of(ApprovalStep.single("manager", "Manager approval", "maria")),
@@ -298,6 +302,7 @@ class JdbcOrdoEngineIntegrationTest {
new JdbcApprovalTaskRepository(connectionProvider),
new JdbcProcessHistoryRepository(connectionProvider),
new JdbcActionExecutionRepository(connectionProvider),
new JdbcInstanceTokenRepository(connectionProvider),
List.of());
actionEngine.publish(new ProcessDefinition("leave-action", "Leave request", List.of(
ApprovalStep.single("manager", "Manager approval", "maria"),
@@ -323,6 +328,29 @@ class JdbcOrdoEngineIntegrationTest {
assertTrue(types.contains(ProcessEventType.INSTANCE_APPROVED));
}
@Test
void joinsAParallelBlock() {
engine.publish(new ProcessDefinition("parallel-review", "P", List.of(
ApprovalStep.parallel("dept", "Dept", List.of(
new ParallelBranch("legal", List.of("legal")),
new ParallelBranch("finance", List.of("finance")))),
ApprovalStep.single("legal", "Legal", "lee"),
ApprovalStep.single("finance", "Finance", "fay"),
ApprovalStep.single("ceo", "CEO", "cara")),
List.of(
StepTransition.end("legal"),
StepTransition.end("finance"),
StepTransition.always("dept", "ceo"),
StepTransition.end("ceo"))));
ProcessInstance instance = engine.start("parallel-review", "alice");
assertEquals(2, engine.findPendingTasksByInstanceId(instance.id()).size());
for (ApprovalTask task : List.copyOf(engine.findPendingTasksByInstanceId(instance.id()))) {
engine.approve(task.id(), task.assignee());
}
engine.approve(engine.findPendingTasksByInstanceId(instance.id()).get(0).id(), "cara");
assertEquals(ProcessStatus.APPROVED, engine.findInstance(instance.id()).orElseThrow().status());
}
private OrdoEngine newEngine(AssigneeResolver assigneeResolver) {
return new DefaultOrdoEngine(Clock.fixed(NOW, ZoneOffset.UTC), assigneeResolver, RoutingCondition.always(),
ActionHandler.noop(),
@@ -332,6 +360,7 @@ class JdbcOrdoEngineIntegrationTest {
new JdbcApprovalTaskRepository(connectionProvider),
new JdbcProcessHistoryRepository(connectionProvider),
new JdbcActionExecutionRepository(connectionProvider),
new JdbcInstanceTokenRepository(connectionProvider),
List.of());
}
}
@@ -5,11 +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.ParallelBranch;
import com.jetlumen.ordo.api.ProcessContext;
import com.jetlumen.ordo.api.ProcessDefinition;
import com.jetlumen.ordo.api.ProcessInstance;
import com.jetlumen.ordo.api.ProcessStatus;
import com.jetlumen.ordo.api.RoutingCondition;
import com.jetlumen.ordo.api.StepTransition;
import com.jetlumen.ordo.api.TaskAction;
import com.jetlumen.ordo.api.TaskStatus;
import com.jetlumen.ordo.core.DefaultOrdoEngine;
@@ -121,6 +123,19 @@ class JdbcPostgresIntegrationTest {
assertEquals("LEAVE-2026-001", finished.context().value("requestId").orElseThrow());
}
@Test
void engineJoinsAParallelBlock() {
OrdoEngine engine = newEngine(AssigneeResolver.direct());
engine.publish(parallelReview("parallel-pg"));
ProcessInstance instance = engine.start("parallel-pg", "alice");
assertEquals(2, engine.findPendingTasksByInstanceId(instance.id()).size());
for (ApprovalTask task : engine.findPendingTasksByInstanceId(instance.id())) {
engine.approve(task.id(), task.assignee());
}
engine.approve(engine.findPendingTasksByInstanceId(instance.id()).get(0).id(), "cara");
assertEquals(ProcessStatus.APPROVED, engine.findInstance(instance.id()).orElseThrow().status());
}
@Test
void publishIsIdempotentForTheSameGraphAndVersionsAChangedGraph() {
JdbcProcessDefinitionRepository repository = new JdbcProcessDefinitionRepository(connectionProvider);
@@ -229,9 +244,25 @@ class JdbcPostgresIntegrationTest {
new JdbcApprovalTaskRepository(connectionProvider),
new JdbcProcessHistoryRepository(connectionProvider),
new JdbcActionExecutionRepository(connectionProvider),
new JdbcInstanceTokenRepository(connectionProvider),
List.of());
}
private static ProcessDefinition parallelReview(String id) {
return new ProcessDefinition(id, "P", List.of(
ApprovalStep.parallel("dept", "Dept", List.of(
new ParallelBranch("legal", List.of("legal")),
new ParallelBranch("finance", List.of("finance")))),
ApprovalStep.single("legal", "Legal", "lee"),
ApprovalStep.single("finance", "Finance", "fay"),
ApprovalStep.single("ceo", "CEO", "cara")),
List.of(
StepTransition.end("legal"),
StepTransition.end("finance"),
StepTransition.always("dept", "ceo"),
StepTransition.end("ceo")));
}
private static PGSimpleDataSource newDataSource(String currentSchema) {
PGSimpleDataSource pgDataSource = new PGSimpleDataSource();
pgDataSource.setServerNames(new String[]{JdbcTestSupport.config("ordo.test.pg.host", "ORDO_TEST_PG_HOST", "localhost")});
@@ -2,6 +2,7 @@ package com.jetlumen.ordo.storage.jdbc;
import com.jetlumen.ordo.api.ApprovalPolicy;
import com.jetlumen.ordo.api.ApprovalStep;
import com.jetlumen.ordo.api.ParallelBranch;
import com.jetlumen.ordo.api.ProcessDefinition;
import com.jetlumen.ordo.api.StepDue;
import com.jetlumen.ordo.api.StepKind;
@@ -97,6 +98,24 @@ class JdbcProcessDefinitionRepositoryTest {
assertEquals("leave-approved-mail", repository.findLatest("notify").orElseThrow().steps().get(1).actionKey());
}
@Test
void insertsAndReadsBackAParallelBlock() {
ProcessDefinition definition = new ProcessDefinition("parallel-review", "P", List.of(
ApprovalStep.parallel("dept", "Dept", List.of(
new ParallelBranch("legal", List.of("legal")),
new ParallelBranch("finance", List.of("finance")))),
ApprovalStep.single("legal", "Legal", "lee"),
ApprovalStep.single("finance", "Finance", "fay"),
ApprovalStep.single("ceo", "CEO", "cara")),
List.of(
StepTransition.end("legal"),
StepTransition.end("finance"),
StepTransition.always("dept", "ceo"),
StepTransition.end("ceo")));
repository.publish(definition);
assertEquals(definition.withVersion(1), repository.findLatest("parallel-review").orElseThrow());
}
private static ProcessDefinition definition(String id, String name) {
return ProcessDefinition.linear(id, name, List.of(ApprovalStep.single("lead", "Lead approval", "lee")));
}
@@ -15,8 +15,10 @@ import java.util.UUID;
final class JdbcTestSupport {
static final String POSTGRES_BASELINE = "/db/postgresql/migration/V1__baseline.sql";
static final String POSTGRES_V2 = "/db/postgresql/migration/V2__transition_condition_json.sql";
static final String POSTGRES_V3 = "/db/postgresql/migration/V3__parallel_blocks.sql";
static final String MYSQL_BASELINE = "/db/mysql/migration/V1__baseline.sql";
static final String MYSQL_V2 = "/db/mysql/migration/V2__transition_condition_json.sql";
static final String MYSQL_V3 = "/db/mysql/migration/V3__parallel_blocks.sql";
private JdbcTestSupport() {
}
@@ -43,7 +45,7 @@ final class JdbcTestSupport {
}
static void applySchema(DataSource dataSource) {
applySchema(dataSource, POSTGRES_BASELINE, POSTGRES_V2);
applySchema(dataSource, POSTGRES_BASELINE, POSTGRES_V2, POSTGRES_V3);
}
static void applySchema(DataSource dataSource, String... resourcePaths) {