feat: add structured PARALLEL blocks with join-all tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+164
@@ -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;
|
||||
}
|
||||
}
|
||||
+69
-9
@@ -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;
|
||||
|
||||
+11
-2
@@ -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);
|
||||
+33
-1
@@ -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");
|
||||
|
||||
+29
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+31
@@ -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")});
|
||||
|
||||
+19
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user