feat: pin running instances to immutable published definition versions

Replace register/replace with publish so new graphs can ship without rewriting old ones, and keep in-flight work on the version it started with.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
0264408
2026-09-16 10:30:14 +08:00
co-authored by Cursor
parent 8bc628613d
commit 9bda7c417c
35 changed files with 688 additions and 382 deletions
@@ -22,8 +22,6 @@ import com.jetlumen.ordo.api.StepTransition;
import com.jetlumen.ordo.api.TaskAction;
import com.jetlumen.ordo.api.TaskStatus;
import com.jetlumen.ordo.api.TransactionExecutor;
import com.jetlumen.ordo.api.exception.DefinitionAlreadyExistsException;
import com.jetlumen.ordo.api.exception.DefinitionInUseException;
import com.jetlumen.ordo.api.exception.DefinitionNotFoundException;
import com.jetlumen.ordo.api.exception.InstanceAlreadyCompletedException;
import com.jetlumen.ordo.api.exception.InstanceNotFoundException;
@@ -102,26 +100,9 @@ public final class DefaultOrdoEngine implements OrdoEngine {
}
@Override
public synchronized void register(ProcessDefinition definition) {
public synchronized ProcessDefinition publish(ProcessDefinition definition) {
Objects.requireNonNull(definition, "definition must not be null");
transactionExecutor.execute(() -> {
if (!definitionRepository.insertIfAbsent(definition)) {
throw new DefinitionAlreadyExistsException(definition.id());
}
return null;
});
}
@Override
public synchronized void replace(ProcessDefinition definition) {
Objects.requireNonNull(definition, "definition must not be null");
transactionExecutor.execute(() -> {
if (instanceRepository.existsRunning(definition.id())) {
throw new DefinitionInUseException(definition.id());
}
definitionRepository.upsert(definition);
return null;
});
return transactionExecutor.execute(() -> definitionRepository.publish(definition));
}
@Override
@@ -131,9 +112,9 @@ public final class DefaultOrdoEngine implements OrdoEngine {
List<PendingAction> queued = new ArrayList<>();
List<ProcessEvent> events = new ArrayList<>();
ProcessInstance instance = transactionExecutor.execute(() -> {
ProcessDefinition definition = requireDefinition(definitionId);
ProcessDefinition definition = requireLatestDefinition(definitionId);
Instant now = clock.instant();
ProcessInstance started = new ProcessInstance(nextId(), definition.id(), initiator,
ProcessInstance started = new ProcessInstance(nextId(), definition.id(), definition.version(), initiator,
ProcessStatus.RUNNING, now, null, context);
instanceRepository.insert(started);
record(events, started.id(), null, null, ProcessEventType.INSTANCE_STARTED, initiator, null, now);
@@ -232,8 +213,9 @@ public final class DefaultOrdoEngine implements OrdoEngine {
throw new InstanceAlreadyCompletedException(instanceId);
}
Instant now = clock.instant();
ProcessInstance finished = new ProcessInstance(instance.id(), instance.definitionId(), instance.initiator(),
status, instance.startedAt(), now, instance.context());
ProcessInstance finished = new ProcessInstance(instance.id(), instance.definitionId(),
instance.definitionVersion(), instance.initiator(), status, instance.startedAt(), now,
instance.context());
if (!instanceRepository.completeIfRunning(finished)) {
throw new InstanceAlreadyCompletedException(instanceId);
}
@@ -295,12 +277,34 @@ public final class DefaultOrdoEngine implements OrdoEngine {
return instanceRepository.query(query, pageRequest);
}
@Override
public synchronized Optional<ProcessDefinition> findDefinition(String definitionId) {
requireText(definitionId, "definition id");
return definitionRepository.findLatest(definitionId);
}
@Override
public synchronized Optional<ProcessDefinition> findDefinition(String definitionId, int version) {
requireText(definitionId, "definition id");
if (version < 1) {
throw new IllegalArgumentException("definition version must be positive");
}
return definitionRepository.find(definitionId, version);
}
@Override
public synchronized Page<ProcessDefinition> queryDefinitions(PageRequest pageRequest) {
Objects.requireNonNull(pageRequest, "pageRequest must not be null");
return definitionRepository.findAll(pageRequest);
}
@Override
public synchronized Page<ProcessDefinition> queryDefinitionVersions(String definitionId, PageRequest pageRequest) {
requireText(definitionId, "definition id");
Objects.requireNonNull(pageRequest, "pageRequest must not be null");
return definitionRepository.findVersions(definitionId, pageRequest);
}
@Override
public synchronized Page<ProcessEvent> queryHistory(String instanceId, PageRequest pageRequest) {
requireText(instanceId, "instance id");
@@ -345,7 +349,7 @@ public final class DefaultOrdoEngine implements OrdoEngine {
if (instance.status() != ProcessStatus.RUNNING) {
return false;
}
ProcessDefinition definition = requireDefinition(instance.definitionId());
ProcessDefinition definition = requireDefinition(instance);
ApprovalStep step = requireStep(definition, overdue.stepId());
StepDue due = step.due();
if (due == null) {
@@ -454,7 +458,7 @@ public final class DefaultOrdoEngine implements OrdoEngine {
private void advanceAfterDecision(ApprovalTask completedTask, Instant now, List<PendingAction> queued,
List<ProcessEvent> events) {
ProcessInstance instance = requireInstance(completedTask.instanceId());
ProcessDefinition definition = requireDefinition(instance.definitionId());
ProcessDefinition definition = requireDefinition(instance);
ApprovalStep step = requireStep(definition, completedTask.stepId());
List<ApprovalTask> siblings = taskRepository.findByInstanceIdAndStepId(instance.id(), step.id());
@@ -600,8 +604,9 @@ public final class DefaultOrdoEngine implements OrdoEngine {
private void completeInstance(ProcessInstance instance, ProcessStatus status, Instant now,
List<ProcessEvent> events) {
ProcessInstance completed = new ProcessInstance(instance.id(), instance.definitionId(), instance.initiator(),
status, instance.startedAt(), now, instance.context());
ProcessInstance completed = new ProcessInstance(instance.id(), instance.definitionId(),
instance.definitionVersion(), instance.initiator(), status, instance.startedAt(), now,
instance.context());
if (!instanceRepository.completeIfRunning(completed)) {
throw new InstanceAlreadyCompletedException(instance.id());
}
@@ -610,11 +615,17 @@ public final class DefaultOrdoEngine implements OrdoEngine {
record(events, instance.id(), null, null, type, null, null, now);
}
private ProcessDefinition requireDefinition(String definitionId) {
return definitionRepository.findById(definitionId)
private ProcessDefinition requireLatestDefinition(String definitionId) {
return definitionRepository.findLatest(definitionId)
.orElseThrow(() -> new DefinitionNotFoundException(definitionId));
}
private ProcessDefinition requireDefinition(ProcessInstance instance) {
return definitionRepository.find(instance.definitionId(), instance.definitionVersion())
.orElseThrow(() -> new DefinitionNotFoundException(instance.definitionId(),
instance.definitionVersion()));
}
private ProcessInstance requireInstance(String instanceId) {
return instanceRepository.findById(instanceId)
.orElseThrow(() -> new IllegalStateException("instance not found: " + instanceId));
@@ -76,13 +76,8 @@ public final class InMemoryOrdoEngine implements OrdoEngine {
}
@Override
public void register(ProcessDefinition definition) {
delegate.register(definition);
}
@Override
public void replace(ProcessDefinition definition) {
delegate.replace(definition);
public ProcessDefinition publish(ProcessDefinition definition) {
return delegate.publish(definition);
}
@Override
@@ -150,11 +145,26 @@ public final class InMemoryOrdoEngine implements OrdoEngine {
return delegate.queryInstances(query, pageRequest);
}
@Override
public Optional<ProcessDefinition> findDefinition(String definitionId) {
return delegate.findDefinition(definitionId);
}
@Override
public Optional<ProcessDefinition> findDefinition(String definitionId, int version) {
return delegate.findDefinition(definitionId, version);
}
@Override
public Page<ProcessDefinition> queryDefinitions(PageRequest pageRequest) {
return delegate.queryDefinitions(pageRequest);
}
@Override
public Page<ProcessDefinition> queryDefinitionVersions(String definitionId, PageRequest pageRequest) {
return delegate.queryDefinitionVersions(definitionId, pageRequest);
}
@Override
public Page<ProcessEvent> queryHistory(String instanceId, PageRequest pageRequest) {
return delegate.queryHistory(instanceId, pageRequest);
@@ -5,35 +5,63 @@ import com.jetlumen.ordo.api.query.Page;
import com.jetlumen.ordo.api.query.PageRequest;
import com.jetlumen.ordo.api.repository.ProcessDefinitionRepository;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
/** Development-only in-memory implementation of the definition storage port. */
public final class InMemoryProcessDefinitionRepository implements ProcessDefinitionRepository {
private final Map<String, ProcessDefinition> definitions = new HashMap<>();
private final Map<String, Integer> latestVersions = new HashMap<>();
private final Map<String, Map<Integer, ProcessDefinition>> versions = new HashMap<>();
@Override
public synchronized boolean insertIfAbsent(ProcessDefinition definition) {
return definitions.putIfAbsent(definition.id(), definition) == null;
public synchronized ProcessDefinition publish(ProcessDefinition definition) {
Objects.requireNonNull(definition, "definition must not be null");
Integer current = latestVersions.get(definition.id());
if (current == null) {
ProcessDefinition first = definition.withVersion(1);
versions.computeIfAbsent(definition.id(), id -> new HashMap<>()).put(1, first);
latestVersions.put(definition.id(), 1);
return first;
}
ProcessDefinition latest = versions.get(definition.id()).get(current);
if (latest.sameGraph(definition)) {
return latest;
}
int next = current + 1;
ProcessDefinition published = definition.withVersion(next);
versions.get(definition.id()).put(next, published);
latestVersions.put(definition.id(), next);
return published;
}
@Override
public synchronized void upsert(ProcessDefinition definition) {
definitions.put(definition.id(), definition);
public synchronized Optional<ProcessDefinition> findLatest(String definitionId) {
Integer version = latestVersions.get(definitionId);
if (version == null) {
return Optional.empty();
}
return Optional.of(versions.get(definitionId).get(version));
}
@Override
public synchronized Optional<ProcessDefinition> findById(String definitionId) {
return Optional.ofNullable(definitions.get(definitionId));
public synchronized Optional<ProcessDefinition> find(String definitionId, int version) {
Map<Integer, ProcessDefinition> byVersion = versions.get(definitionId);
if (byVersion == null) {
return Optional.empty();
}
return Optional.ofNullable(byVersion.get(version));
}
@Override
public synchronized Page<ProcessDefinition> findAll(PageRequest pageRequest) {
List<ProcessDefinition> sorted = definitions.values().stream()
.sorted(Comparator.comparing(ProcessDefinition::id))
List<ProcessDefinition> sorted = latestVersions.keySet().stream()
.sorted(Comparator.naturalOrder())
.map(id -> versions.get(id).get(latestVersions.get(id)))
.toList();
List<ProcessDefinition> page = sorted.stream()
.skip((long) pageRequest.offset())
@@ -41,4 +69,16 @@ public final class InMemoryProcessDefinitionRepository implements ProcessDefinit
.toList();
return new Page<>(page, sorted.size(), pageRequest.page(), pageRequest.size());
}
@Override
public synchronized Page<ProcessDefinition> findVersions(String definitionId, PageRequest pageRequest) {
Map<Integer, ProcessDefinition> byVersion = versions.getOrDefault(definitionId, Map.of());
List<ProcessDefinition> sorted = new ArrayList<>(byVersion.values());
sorted.sort(Comparator.comparingInt(ProcessDefinition::version).reversed());
List<ProcessDefinition> page = sorted.stream()
.skip((long) pageRequest.offset())
.limit(pageRequest.size())
.toList();
return new Page<>(page, sorted.size(), pageRequest.page(), pageRequest.size());
}
}
@@ -17,8 +17,6 @@ import com.jetlumen.ordo.api.StepDue;
import com.jetlumen.ordo.api.StepKind;
import com.jetlumen.ordo.api.StepTransition;
import com.jetlumen.ordo.api.TaskStatus;
import com.jetlumen.ordo.api.exception.DefinitionAlreadyExistsException;
import com.jetlumen.ordo.api.exception.DefinitionInUseException;
import com.jetlumen.ordo.api.exception.DefinitionNotFoundException;
import com.jetlumen.ordo.api.exception.InstanceAlreadyCompletedException;
import com.jetlumen.ordo.api.exception.InstanceNotFoundException;
@@ -53,7 +51,7 @@ class InMemoryOrdoEngineTest {
@BeforeEach
void setUp() {
engine = new InMemoryOrdoEngine();
engine.register(ProcessDefinition.linear("leave", "Leave request", List.of(
engine.publish(ProcessDefinition.linear("leave", "Leave request", List.of(
ApprovalStep.single("manager", "Manager approval", "maria"),
ApprovalStep.single("hr", "HR approval", "henry")
)));
@@ -136,7 +134,7 @@ class InMemoryOrdoEngineTest {
@Test
void reassignDoesNotAdvanceAnAllPolicyStepAndRejectsDuplicatePendingAssignee() {
InMemoryOrdoEngine allEngine = new InMemoryOrdoEngine();
allEngine.register(ProcessDefinition.linear("leave-all-reassign", "Leave request", List.of(
allEngine.publish(ProcessDefinition.linear("leave-all-reassign", "Leave request", List.of(
new ApprovalStep("manager", "Manager approval", List.of("maria", "mike"), ApprovalPolicy.ALL)
)));
var instance = allEngine.start("leave-all-reassign", "alice");
@@ -157,51 +155,56 @@ class InMemoryOrdoEngineTest {
}
@Test
void exposesSpecificExceptionsForMissingAndDuplicateResources() {
void exposesSpecificExceptionsForMissingResources() {
assertThrows(DefinitionNotFoundException.class, () -> engine.start("missing", "alice"));
assertThrows(TaskNotFoundException.class, () -> engine.approve("missing", "maria"));
assertThrows(DefinitionAlreadyExistsException.class, () -> engine.register(ProcessDefinition.linear(
"leave", "Another leave request", List.of(ApprovalStep.single("lead", "Lead approval", "lee")))));
}
@Test
void replaceInsertsWhenTheDefinitionIsMissing() {
void publishInsertsVersionOneWhenTheDefinitionIsMissing() {
InMemoryOrdoEngine empty = new InMemoryOrdoEngine();
empty.replace(ProcessDefinition.linear("expense", "Expense request", List.of(
ProcessDefinition published = empty.publish(ProcessDefinition.linear("expense", "Expense request", List.of(
ApprovalStep.single("director", "Director approval", "diana"))));
assertEquals(1, published.version());
var instance = empty.start("expense", "alice");
assertEquals(1, instance.definitionVersion());
assertEquals("diana", empty.findTasks(instance.id()).get(0).assignee());
}
@Test
void replaceSwapsTheGraphWhenNoInstanceIsRunning() {
engine.replace(ProcessDefinition.linear("leave", "Leave request v2", List.of(
ApprovalStep.single("director", "Director approval", "diana"))));
var instance = engine.start("leave", "alice");
ApprovalTask task = engine.findTasks(instance.id()).get(0);
assertEquals("director", task.stepId());
assertEquals("diana", task.assignee());
void publishIsIdempotentWhenTheGraphIsUnchanged() {
ProcessDefinition first = engine.findDefinition("leave").orElseThrow();
ProcessDefinition second = engine.publish(ProcessDefinition.linear("leave", "Leave request", List.of(
ApprovalStep.single("manager", "Manager approval", "maria"),
ApprovalStep.single("hr", "HR approval", "henry")
)));
assertEquals(first, second);
assertEquals(1, engine.queryDefinitionVersions("leave", new PageRequest(0, 10)).totalElements());
}
@Test
void replaceIsRejectedWhileAnInstanceIsRunning() {
engine.start("leave", "alice");
assertThrows(DefinitionInUseException.class, () -> engine.replace(ProcessDefinition.linear(
"leave", "Leave request v2", List.of(ApprovalStep.single("director", "Director approval", "diana")))));
}
void publishCreatesANewVersionAndLocksRunningInstancesToTheOldGraph() {
var running = engine.start("leave", "alice");
assertEquals(1, running.definitionVersion());
@Test
void replaceSucceedsAfterInstancesReachATerminalStatus() {
var instance = engine.start("leave", "alice");
engine.reject(engine.findTasks(instance.id()).get(0).id(), "maria");
engine.replace(ProcessDefinition.linear("leave", "Leave request v2", List.of(
engine.publish(ProcessDefinition.linear("leave", "Leave request v2", List.of(
ApprovalStep.single("director", "Director approval", "diana"))));
assertEquals(2, engine.findDefinition("leave").orElseThrow().version());
assertEquals("maria", engine.findTasks(running.id()).get(0).assignee());
engine.approve(engine.findTasks(running.id()).get(0).id(), "maria");
assertEquals("hr", engine.findPendingTasksByInstanceId(running.id()).get(0).stepId());
var next = engine.start("leave", "bob");
assertEquals(2, next.definitionVersion());
assertEquals("diana", engine.findTasks(next.id()).get(0).assignee());
Page<ProcessDefinition> latest = engine.queryDefinitions(new PageRequest(0, 10));
assertEquals(List.of("leave"), latest.content().stream().map(ProcessDefinition::id).toList());
assertEquals(List.of(2), latest.content().stream().map(ProcessDefinition::version).toList());
Page<ProcessDefinition> versions = engine.queryDefinitionVersions("leave", new PageRequest(0, 10));
assertEquals(List.of(2, 1), versions.content().stream().map(ProcessDefinition::version).toList());
}
@Test
@@ -352,7 +355,7 @@ class InMemoryOrdoEngineTest {
.filter(String.class::isInstance)
.map(String.class::cast)
.orElse(candidate));
contextAwareEngine.register(ProcessDefinition.linear("leave", "Leave request", List.of(
contextAwareEngine.publish(ProcessDefinition.linear("leave", "Leave request", List.of(
ApprovalStep.single("manager", "Manager approval", "maria"),
ApprovalStep.single("hr", "HR approval", "henry")
)));
@@ -372,7 +375,7 @@ class InMemoryOrdoEngineTest {
@Test
void anyPolicyAdvancesOnFirstApprovalAndSkipsTheOtherCandidates() {
InMemoryOrdoEngine anyEngine = new InMemoryOrdoEngine();
anyEngine.register(ProcessDefinition.linear("leave-any", "Leave request", List.of(
anyEngine.publish(ProcessDefinition.linear("leave-any", "Leave request", List.of(
new ApprovalStep("manager", "Manager approval", List.of("maria", "mike"), ApprovalPolicy.ANY),
ApprovalStep.single("hr", "HR approval", "henry")
)));
@@ -401,7 +404,7 @@ class InMemoryOrdoEngineTest {
@Test
void anyPolicyOnlyRejectsTheStepOnceEveryCandidateHasRejected() {
InMemoryOrdoEngine anyEngine = new InMemoryOrdoEngine();
anyEngine.register(ProcessDefinition.linear("leave-any-reject", "Leave request", List.of(
anyEngine.publish(ProcessDefinition.linear("leave-any-reject", "Leave request", List.of(
new ApprovalStep("manager", "Manager approval", List.of("maria", "mike"), ApprovalPolicy.ANY)
)));
@@ -421,7 +424,7 @@ class InMemoryOrdoEngineTest {
@Test
void allPolicyOnlyAdvancesOnceEveryCandidateHasApproved() {
InMemoryOrdoEngine allEngine = new InMemoryOrdoEngine();
allEngine.register(ProcessDefinition.linear("leave-all", "Leave request", List.of(
allEngine.publish(ProcessDefinition.linear("leave-all", "Leave request", List.of(
new ApprovalStep("manager", "Manager approval", List.of("maria", "mike"), ApprovalPolicy.ALL),
ApprovalStep.single("hr", "HR approval", "henry")
)));
@@ -446,7 +449,7 @@ class InMemoryOrdoEngineTest {
@Test
void allPolicyFailsFastAndSkipsRemainingCandidatesOnASingleRejection() {
InMemoryOrdoEngine allEngine = new InMemoryOrdoEngine();
allEngine.register(ProcessDefinition.linear("leave-all-reject", "Leave request", List.of(
allEngine.publish(ProcessDefinition.linear("leave-all-reject", "Leave request", List.of(
new ApprovalStep("manager", "Manager approval", List.of("maria", "mike"), ApprovalPolicy.ALL)
)));
@@ -478,7 +481,7 @@ class InMemoryOrdoEngineTest {
List<ApprovalStep> steps = List.of(
ApprovalStep.single("manager", "Manager approval", "maria"),
ApprovalStep.single("director", "Director approval", "diana"));
routingEngine.register(new ProcessDefinition("expense", "Expense request", steps, List.of(
routingEngine.publish(new ProcessDefinition("expense", "Expense request", steps, List.of(
StepTransition.when("manager", "director", "amount-gt-1000", 0),
new StepTransition("manager", null, null, 1),
StepTransition.end("director"))));
@@ -500,7 +503,7 @@ class InMemoryOrdoEngineTest {
List<String> executed = new java.util.ArrayList<>();
InMemoryOrdoEngine actionEngine = new InMemoryOrdoEngine(Clock.systemUTC(), AssigneeResolver.direct(),
RoutingCondition.always(), (key, context) -> executed.add(key));
actionEngine.register(new ProcessDefinition("leave", "Leave request", List.of(
actionEngine.publish(new ProcessDefinition("leave", "Leave request", List.of(
ApprovalStep.single("manager", "Manager approval", "maria"),
ApprovalStep.action("notify", "Notify HR", "leave-approved-mail"),
ApprovalStep.single("hr", "HR approval", "henry")),
@@ -524,7 +527,7 @@ class InMemoryOrdoEngineTest {
RoutingCondition.always(), (key, context) -> {
throw new IllegalStateException("mail failed");
});
actionEngine.register(new ProcessDefinition("leave", "Leave request", List.of(
actionEngine.publish(new ProcessDefinition("leave", "Leave request", List.of(
ApprovalStep.single("manager", "Manager approval", "maria"),
ApprovalStep.action("notify", "Notify HR", "leave-approved-mail")),
List.of(
@@ -541,7 +544,7 @@ class InMemoryOrdoEngineTest {
List<String> executed = new java.util.ArrayList<>();
InMemoryOrdoEngine actionEngine = new InMemoryOrdoEngine(Clock.systemUTC(), AssigneeResolver.direct(),
RoutingCondition.always(), (key, context) -> executed.add(key));
actionEngine.register(new ProcessDefinition("leave", "Leave request", List.of(
actionEngine.publish(new ProcessDefinition("leave", "Leave request", List.of(
ApprovalStep.action("notify", "Notify manager", "leave-submitted-mail"),
ApprovalStep.single("manager", "Manager approval", "maria")),
List.of(
@@ -557,7 +560,7 @@ class InMemoryOrdoEngineTest {
@Test
void throwsWhenNoTransitionMatches() {
InMemoryOrdoEngine routingEngine = new InMemoryOrdoEngine((key, context) -> false);
routingEngine.register(new ProcessDefinition("expense", "Expense request",
routingEngine.publish(new ProcessDefinition("expense", "Expense request",
List.of(ApprovalStep.single("manager", "Manager approval", "maria")),
List.of(StepTransition.endWhen("manager", "never", 0))));
@@ -568,7 +571,7 @@ class InMemoryOrdoEngineTest {
@Test
void queryTasksFiltersByAssigneeDefinitionAndPaginates() {
engine.register(ProcessDefinition.linear("expense", "Expense request", List.of(
engine.publish(ProcessDefinition.linear("expense", "Expense request", List.of(
ApprovalStep.single("finance", "Finance approval", "frank"))));
var leaveInstance = engine.start("leave", "alice");
engine.start("expense", "alice");
@@ -614,7 +617,7 @@ class InMemoryOrdoEngineTest {
@Test
void queryDefinitionsPaginatesRegisteredDefinitions() {
engine.register(ProcessDefinition.linear("expense", "Expense request", List.of(
engine.publish(ProcessDefinition.linear("expense", "Expense request", List.of(
ApprovalStep.single("finance", "Finance approval", "frank"))));
Page<ProcessDefinition> all = engine.queryDefinitions(new PageRequest(0, 10));
@@ -625,7 +628,7 @@ class InMemoryOrdoEngineTest {
@Test
void recordsHistoryForStartApproveSkipAndComplete() {
InMemoryOrdoEngine anyEngine = new InMemoryOrdoEngine();
anyEngine.register(ProcessDefinition.linear("leave-any", "Leave request", List.of(
anyEngine.publish(ProcessDefinition.linear("leave-any", "Leave request", List.of(
new ApprovalStep("manager", "Manager approval", List.of("maria", "mike"), ApprovalPolicy.ANY),
ApprovalStep.single("hr", "HR approval", "henry")
)));
@@ -690,7 +693,7 @@ class InMemoryOrdoEngineTest {
}
received.add(event.type());
}));
listening.register(ProcessDefinition.linear("leave", "Leave request", List.of(
listening.publish(ProcessDefinition.linear("leave", "Leave request", List.of(
ApprovalStep.single("manager", "Manager approval", "maria"))));
var instance = listening.start("leave", "alice");
@@ -711,7 +714,7 @@ class InMemoryOrdoEngineTest {
throw new IllegalStateException("mail failed");
}
});
actionEngine.register(new ProcessDefinition("leave", "Leave request", List.of(
actionEngine.publish(new ProcessDefinition("leave", "Leave request", List.of(
ApprovalStep.single("manager", "Manager approval", "maria"),
ApprovalStep.action("notify", "Notify HR", "ok-mail"),
ApprovalStep.action("fail", "Fail mail", "fail-mail")),
@@ -743,7 +746,7 @@ class InMemoryOrdoEngineTest {
void processDueReassignsAfterTheStepDueElapses() {
MutableClock clock = new MutableClock(Instant.parse("2026-01-15T09:00:00Z"));
InMemoryOrdoEngine dueEngine = new InMemoryOrdoEngine(clock);
dueEngine.register(ProcessDefinition.linear("leave-due", "Leave request", List.of(
dueEngine.publish(ProcessDefinition.linear("leave-due", "Leave request", List.of(
new ApprovalStep("manager", "Manager approval", List.of("maria"), ApprovalPolicy.ANY, StepKind.APPROVAL,
null, StepDue.reassign(java.time.Duration.ofHours(1), "diana")))));
@@ -770,7 +773,7 @@ class InMemoryOrdoEngineTest {
List<String> actions = new java.util.ArrayList<>();
InMemoryOrdoEngine dueEngine = new InMemoryOrdoEngine(clock, AssigneeResolver.direct(),
RoutingCondition.always(), (key, context) -> actions.add(key));
dueEngine.register(ProcessDefinition.linear("leave-notify", "Leave request", List.of(
dueEngine.publish(ProcessDefinition.linear("leave-notify", "Leave request", List.of(
new ApprovalStep("manager", "Manager approval", List.of("maria"), ApprovalPolicy.ANY, StepKind.APPROVAL,
null, StepDue.notify(java.time.Duration.ofMinutes(30), "overdue-mail")))));
@@ -786,7 +789,7 @@ class InMemoryOrdoEngineTest {
void processDueGotoSkipsTheStepAndEntersTheTarget() {
MutableClock clock = new MutableClock(Instant.parse("2026-01-15T09:00:00Z"));
InMemoryOrdoEngine dueEngine = new InMemoryOrdoEngine(clock);
dueEngine.register(new ProcessDefinition("leave-goto", "Leave request", List.of(
dueEngine.publish(new ProcessDefinition("leave-goto", "Leave request", List.of(
new ApprovalStep("manager", "Manager approval", List.of("maria"), ApprovalPolicy.ANY, StepKind.APPROVAL,
null, StepDue.gotoStep(java.time.Duration.ofHours(1), "hr")),
ApprovalStep.single("hr", "HR approval", "henry")),
@@ -61,9 +61,9 @@ class InMemoryApprovalTaskRepositoryTest {
@Test
void definitionIdFilterResolvesThroughTheInstanceRepository() {
InMemoryProcessInstanceRepository instanceRepository = new InMemoryProcessInstanceRepository();
instanceRepository.insert(new ProcessInstance("inst-1", "leave", "alice", ProcessStatus.RUNNING,
instanceRepository.insert(new ProcessInstance("inst-1", "leave", 1, "alice", ProcessStatus.RUNNING,
CREATED_AT, null, ProcessContext.empty()));
instanceRepository.insert(new ProcessInstance("inst-2", "expense", "alice", ProcessStatus.RUNNING,
instanceRepository.insert(new ProcessInstance("inst-2", "expense", 1, "alice", ProcessStatus.RUNNING,
CREATED_AT, null, ProcessContext.empty()));
InMemoryApprovalTaskRepository repository = new InMemoryApprovalTaskRepository(instanceRepository);
ApprovalTask leaveTask = task("task-1", "inst-1", "maria", TaskStatus.PENDING, CREATED_AT);
@@ -14,11 +14,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class InMemoryProcessDefinitionRepositoryTest {
@Test
void findAllPaginatesDefinitionsOrderedById() {
void findAllPaginatesLatestDefinitionsOrderedById() {
InMemoryProcessDefinitionRepository repository = new InMemoryProcessDefinitionRepository();
repository.insertIfAbsent(definition("c-def"));
repository.insertIfAbsent(definition("a-def"));
repository.insertIfAbsent(definition("b-def"));
repository.publish(definition("c-def"));
repository.publish(definition("a-def"));
repository.publish(definition("b-def"));
Page<ProcessDefinition> pageOne = repository.findAll(new PageRequest(0, 2));
assertEquals(3, pageOne.totalElements());
@@ -31,6 +31,20 @@ class InMemoryProcessDefinitionRepositoryTest {
assertFalse(pageTwo.hasNext());
}
@Test
void publishKeepsPreviousVersions() {
InMemoryProcessDefinitionRepository repository = new InMemoryProcessDefinitionRepository();
repository.publish(ProcessDefinition.linear("leave", "v1", List.of(
ApprovalStep.single("lead", "Lead approval", "lee"))));
repository.publish(ProcessDefinition.linear("leave", "v2", List.of(
ApprovalStep.single("director", "Director approval", "diana"))));
assertEquals(2, repository.findLatest("leave").orElseThrow().version());
assertEquals("v1", repository.find("leave", 1).orElseThrow().name());
Page<ProcessDefinition> versions = repository.findVersions("leave", new PageRequest(0, 10));
assertEquals(List.of(2, 1), versions.content().stream().map(ProcessDefinition::version).toList());
}
private static ProcessDefinition definition(String id) {
return ProcessDefinition.linear(id, id, List.of(ApprovalStep.single("lead", "Lead approval", "lee")));
}
@@ -56,6 +56,6 @@ class InMemoryProcessInstanceRepositoryTest {
private static ProcessInstance instance(String id, String definitionId, String initiator, ProcessStatus status,
Instant startedAt) {
return new ProcessInstance(id, definitionId, initiator, status, startedAt, null, ProcessContext.empty());
return new ProcessInstance(id, definitionId, 1, initiator, status, startedAt, null, ProcessContext.empty());
}
}