feat: pass ProcessRuntime to host SPIs
Keep initiator and instance identity off ProcessContext variables so AssigneeResolver, RoutingCondition, and ActionHandler can read them from a dedicated runtime view. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import com.jetlumen.ordo.api.OrdoEventListener;
|
||||
import com.jetlumen.ordo.api.ProcessContext;
|
||||
import com.jetlumen.ordo.api.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.ProcessEvent;
|
||||
import com.jetlumen.ordo.api.ProcessRuntime;
|
||||
import com.jetlumen.ordo.api.ProcessEventType;
|
||||
import com.jetlumen.ordo.api.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.ProcessStatus;
|
||||
@@ -368,7 +369,7 @@ public final class DefaultOrdoEngine implements OrdoEngine {
|
||||
|
||||
private boolean escalateReassign(ApprovalTask overdue, ProcessInstance instance, ApprovalStep step, StepDue due,
|
||||
Instant now, List<ProcessEvent> events) {
|
||||
String newAssignee = assigneeResolver.resolve(due.to(), step, instance.context());
|
||||
String newAssignee = assigneeResolver.resolve(due.to(), step, ProcessRuntime.of(instance, step.id()));
|
||||
requireText(newAssignee, "resolved assignee");
|
||||
boolean duplicatePending = taskRepository.findByInstanceIdAndStepId(overdue.instanceId(), overdue.stepId())
|
||||
.stream()
|
||||
@@ -396,8 +397,7 @@ public final class DefaultOrdoEngine implements OrdoEngine {
|
||||
actionExecutionRepository.insert(new ActionExecution(executionId, instance.id(), overdue.stepId(),
|
||||
due.action(), ActionExecutionStatus.PENDING, null,
|
||||
now.plusMillis(eventSequence.getAndIncrement()), null));
|
||||
queued.add(new PendingAction(executionId, due.action(), instance.id(), overdue.stepId(),
|
||||
instance.context()));
|
||||
queued.add(new PendingAction(executionId, due.action(), ProcessRuntime.of(instance, overdue.stepId())));
|
||||
}
|
||||
record(events, overdue.instanceId(), overdue.id(), overdue.stepId(), ProcessEventType.TASK_ESCALATED, null,
|
||||
due.action(), now);
|
||||
@@ -417,7 +417,7 @@ public final class DefaultOrdoEngine implements OrdoEngine {
|
||||
|
||||
private void createStepTasks(ProcessInstance instance, ApprovalStep step, Instant now, List<ProcessEvent> events) {
|
||||
for (String candidate : step.candidates()) {
|
||||
String assignee = assigneeResolver.resolve(candidate, step, instance.context());
|
||||
String assignee = assigneeResolver.resolve(candidate, step, ProcessRuntime.of(instance, step.id()));
|
||||
requireText(assignee, "resolved assignee");
|
||||
Instant dueAt = step.due() == null ? null : now.plus(step.due().after());
|
||||
ApprovalTask task = new ApprovalTask(nextId(), instance.id(), step.id(), step.name(), assignee,
|
||||
@@ -525,8 +525,7 @@ public final class DefaultOrdoEngine implements OrdoEngine {
|
||||
actionExecutionRepository.insert(new ActionExecution(executionId, instance.id(), current.id(),
|
||||
current.actionKey(), ActionExecutionStatus.PENDING, null,
|
||||
now.plusMillis(eventSequence.getAndIncrement()), null));
|
||||
queued.add(new PendingAction(executionId, current.actionKey(), instance.id(), current.id(),
|
||||
instance.context()));
|
||||
queued.add(new PendingAction(executionId, current.actionKey(), ProcessRuntime.of(instance, current.id())));
|
||||
touchToken(definition, instance.id(), current.id());
|
||||
StepTransition matched = resolveTransition(definition, current, instance);
|
||||
if (matched.toStepId() == null) {
|
||||
@@ -588,15 +587,15 @@ public final class DefaultOrdoEngine implements OrdoEngine {
|
||||
for (PendingAction pending : queued) {
|
||||
Instant now = clock.instant();
|
||||
try {
|
||||
actionHandler.execute(pending.actionKey(), pending.context());
|
||||
actionHandler.execute(pending.actionKey(), pending.runtime());
|
||||
actionExecutionRepository.complete(pending.executionId(), ActionExecutionStatus.SUCCESS, null, now);
|
||||
record(events, pending.instanceId(), null, pending.stepId(), ProcessEventType.ACTION_SUCCEEDED, null,
|
||||
pending.actionKey(), now);
|
||||
record(events, pending.runtime().instanceId(), null, pending.runtime().stepId(),
|
||||
ProcessEventType.ACTION_SUCCEEDED, null, pending.actionKey(), now);
|
||||
} catch (RuntimeException e) {
|
||||
actionExecutionRepository.complete(pending.executionId(), ActionExecutionStatus.FAILED, e.getMessage(),
|
||||
now);
|
||||
record(events, pending.instanceId(), null, pending.stepId(), ProcessEventType.ACTION_FAILED, null,
|
||||
e.getMessage(), now);
|
||||
record(events, pending.runtime().instanceId(), null, pending.runtime().stepId(),
|
||||
ProcessEventType.ACTION_FAILED, null, e.getMessage(), now);
|
||||
LOG.log(Level.WARNING, "action failed: " + pending.actionKey(), e);
|
||||
}
|
||||
}
|
||||
@@ -632,19 +631,19 @@ public final class DefaultOrdoEngine implements OrdoEngine {
|
||||
outgoing.stream().filter(transition -> transition.when() == null).sorted(byPriority).forEach(candidates::add);
|
||||
for (StepTransition transition : candidates) {
|
||||
RoutingWhen when = transition.when();
|
||||
if (when == null || matches(when, instance.context())) {
|
||||
if (when == null || matches(when, instance, step.id())) {
|
||||
return transition;
|
||||
}
|
||||
}
|
||||
throw new NoRouteFoundException(step.id(), instance.id());
|
||||
}
|
||||
|
||||
private boolean matches(RoutingWhen when, ProcessContext context) {
|
||||
private boolean matches(RoutingWhen when, ProcessInstance instance, String stepId) {
|
||||
if (when instanceof RoutingWhen.Predicate predicate) {
|
||||
return RoutingPredicateEvaluator.matches(predicate.tree(), context);
|
||||
return RoutingPredicateEvaluator.matches(predicate.tree(), instance.context());
|
||||
}
|
||||
RoutingWhen.Ref ref = (RoutingWhen.Ref) when;
|
||||
return routingCondition.matches(ref.key(), ref.args(), context);
|
||||
return routingCondition.matches(ref.key(), ref.args(), ProcessRuntime.of(instance, stepId));
|
||||
}
|
||||
|
||||
/** Marks any still-pending sibling candidate tasks for the same step as skipped. */
|
||||
@@ -721,7 +720,6 @@ public final class DefaultOrdoEngine implements OrdoEngine {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
private record PendingAction(String executionId, String actionKey, String instanceId, String stepId,
|
||||
ProcessContext context) {
|
||||
private record PendingAction(String executionId, String actionKey, ProcessRuntime runtime) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ 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.ProcessRuntime;
|
||||
import com.jetlumen.ordo.api.ProcessStatus;
|
||||
import com.jetlumen.ordo.api.RoutingCondition;
|
||||
import com.jetlumen.ordo.api.RoutingPredicate;
|
||||
@@ -353,8 +354,8 @@ class InMemoryOrdoEngineTest {
|
||||
|
||||
@Test
|
||||
void resolvesAssigneesFromTheProcessContext() {
|
||||
InMemoryOrdoEngine contextAwareEngine = new InMemoryOrdoEngine((AssigneeResolver) (candidate, step, context) ->
|
||||
context.value(step.id())
|
||||
InMemoryOrdoEngine contextAwareEngine = new InMemoryOrdoEngine((AssigneeResolver) (candidate, step, runtime) ->
|
||||
runtime.context().value(step.id())
|
||||
.filter(String.class::isInstance)
|
||||
.map(String.class::cast)
|
||||
.orElse(candidate));
|
||||
@@ -513,12 +514,12 @@ class InMemoryOrdoEngineTest {
|
||||
|
||||
@Test
|
||||
void routesUsingParameterizedHostCondition() {
|
||||
RoutingCondition routingCondition = (key, args, context) -> {
|
||||
RoutingCondition routingCondition = (key, args, runtime) -> {
|
||||
if (!"amountGt".equals(key)) {
|
||||
return false;
|
||||
}
|
||||
Number threshold = (Number) args.get("threshold");
|
||||
return context.value("amount")
|
||||
return runtime.context().value("amount")
|
||||
.filter(Number.class::isInstance)
|
||||
.map(Number.class::cast)
|
||||
.map(amount -> amount.doubleValue() > threshold.doubleValue())
|
||||
@@ -541,8 +542,12 @@ class InMemoryOrdoEngineTest {
|
||||
@Test
|
||||
void runsActionStepsAfterApprovalThenCreatesTheNextApprovalTask() {
|
||||
List<String> executed = new java.util.ArrayList<>();
|
||||
List<ProcessRuntime> runtimes = new java.util.ArrayList<>();
|
||||
InMemoryOrdoEngine actionEngine = new InMemoryOrdoEngine(Clock.systemUTC(), AssigneeResolver.direct(),
|
||||
RoutingCondition.always(), (key, context) -> executed.add(key));
|
||||
RoutingCondition.always(), (key, runtime) -> {
|
||||
executed.add(key);
|
||||
runtimes.add(runtime);
|
||||
});
|
||||
actionEngine.publish(new ProcessDefinition("leave", "Leave request", List.of(
|
||||
ApprovalStep.single("manager", "Manager approval", "maria"),
|
||||
ApprovalStep.action("notify", "Notify HR", "leave-approved-mail"),
|
||||
@@ -556,6 +561,12 @@ class InMemoryOrdoEngineTest {
|
||||
actionEngine.approve(actionEngine.findPendingTasksByInstanceId(instance.id()).get(0).id(), "maria");
|
||||
|
||||
assertEquals(List.of("leave-approved-mail"), executed);
|
||||
var runtime = runtimes.get(0);
|
||||
assertEquals(instance.id(), runtime.instanceId());
|
||||
assertEquals("leave", runtime.definitionId());
|
||||
assertEquals(1, runtime.definitionVersion());
|
||||
assertEquals("alice", runtime.initiator());
|
||||
assertEquals("notify", runtime.stepId());
|
||||
ApprovalTask hrTask = actionEngine.findPendingTasksByInstanceId(instance.id()).get(0);
|
||||
assertEquals("hr", hrTask.stepId());
|
||||
assertEquals(ProcessStatus.RUNNING, actionEngine.findInstance(instance.id()).orElseThrow().status());
|
||||
|
||||
Reference in New Issue
Block a user