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:
@@ -8,15 +8,15 @@ import com.jetlumen.ordo.api.query.TaskQuery;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Public entry point for definition registration and approval operations. */
|
||||
/** Public entry point for definition publication and approval operations. */
|
||||
public interface OrdoEngine {
|
||||
void register(ProcessDefinition definition);
|
||||
|
||||
/**
|
||||
* Inserts the definition, or replaces its entire graph when the id already exists.
|
||||
* Refuses replacement while any instance of this definition is {@code RUNNING}.
|
||||
* Publishes an immutable graph version. First publication of an id is version 1.
|
||||
* If the graph matches the latest version, returns that version without inserting.
|
||||
* Otherwise inserts {@code latest + 1}. Running instances do not block publication.
|
||||
*/
|
||||
void replace(ProcessDefinition definition);
|
||||
ProcessDefinition publish(ProcessDefinition definition);
|
||||
|
||||
default ProcessInstance start(String definitionId, String initiator) {
|
||||
return start(definitionId, initiator, ProcessContext.empty());
|
||||
}
|
||||
@@ -49,15 +49,22 @@ public interface OrdoEngine {
|
||||
List<ApprovalTask> findPendingTasksByAssignee(String assignee);
|
||||
List<ApprovalTask> findPendingTasksByInstanceId(String instanceId);
|
||||
|
||||
Optional<ProcessDefinition> findDefinition(String definitionId);
|
||||
|
||||
Optional<ProcessDefinition> findDefinition(String definitionId, int version);
|
||||
|
||||
/** Paginated, filterable task query; see {@link com.jetlumen.ordo.api.repository.ApprovalTaskRepository#query}. */
|
||||
Page<ApprovalTask> queryTasks(TaskQuery query, PageRequest pageRequest);
|
||||
|
||||
/** Paginated, filterable instance query; see {@link com.jetlumen.ordo.api.repository.ProcessInstanceRepository#query}. */
|
||||
Page<ProcessInstance> queryInstances(InstanceQuery query, PageRequest pageRequest);
|
||||
|
||||
/** Paginated listing of all registered process definitions. */
|
||||
/** Paginated listing of the latest version of each process definition, ordered by id. */
|
||||
Page<ProcessDefinition> queryDefinitions(PageRequest pageRequest);
|
||||
|
||||
/** Paginated versions of one definition, newest version first. */
|
||||
Page<ProcessDefinition> queryDefinitionVersions(String definitionId, PageRequest pageRequest);
|
||||
|
||||
/** Instance timeline, oldest-first; see {@link com.jetlumen.ordo.api.repository.ProcessHistoryRepository#query}. */
|
||||
Page<ProcessEvent> queryHistory(String instanceId, PageRequest pageRequest);
|
||||
|
||||
|
||||
@@ -8,9 +8,13 @@ import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/** Immutable blueprint for an approval process with explicit step transitions. */
|
||||
public record ProcessDefinition(String id, String name, List<ApprovalStep> steps, List<StepTransition> transitions) {
|
||||
public record ProcessDefinition(String id, int version, String name, List<ApprovalStep> steps,
|
||||
List<StepTransition> transitions) {
|
||||
public ProcessDefinition {
|
||||
ApprovalStep.requireText(id, "definition id");
|
||||
if (version < 0) {
|
||||
throw new IllegalArgumentException("definition version must not be negative");
|
||||
}
|
||||
ApprovalStep.requireText(name, "definition name");
|
||||
steps = List.copyOf(steps);
|
||||
if (steps.isEmpty()) {
|
||||
@@ -52,6 +56,24 @@ public record ProcessDefinition(String id, String name, List<ApprovalStep> steps
|
||||
.toList();
|
||||
}
|
||||
|
||||
/** Unpublished graph; the engine assigns a version on {@code publish}. */
|
||||
public ProcessDefinition(String id, String name, List<ApprovalStep> steps, List<StepTransition> transitions) {
|
||||
this(id, 0, name, steps, transitions);
|
||||
}
|
||||
|
||||
public ProcessDefinition withVersion(int version) {
|
||||
return new ProcessDefinition(id, version, name, steps, transitions);
|
||||
}
|
||||
|
||||
/** Equality of the executable graph, ignoring assigned version. */
|
||||
public boolean sameGraph(ProcessDefinition other) {
|
||||
Objects.requireNonNull(other, "other must not be null");
|
||||
return id.equals(other.id)
|
||||
&& name.equals(other.name)
|
||||
&& steps.equals(other.steps)
|
||||
&& transitions.equals(other.transitions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a definition whose transitions mirror the former linear steps order: each step
|
||||
* unconditionally advances to the next, and the last step unconditionally ends.
|
||||
|
||||
@@ -112,6 +112,7 @@ public final class ProcessDefinitionParser {
|
||||
private record DefinitionDocument(
|
||||
String id,
|
||||
String name,
|
||||
Integer version,
|
||||
String startStep,
|
||||
List<StepDocument> steps,
|
||||
List<TransitionDocument> transitions) {
|
||||
|
||||
@@ -2,6 +2,6 @@ package com.jetlumen.ordo.api;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record ProcessInstance(String id, String definitionId, String initiator, ProcessStatus status,
|
||||
Instant startedAt, Instant finishedAt, ProcessContext context) {
|
||||
public record ProcessInstance(String id, String definitionId, int definitionVersion, String initiator,
|
||||
ProcessStatus status, Instant startedAt, Instant finishedAt, ProcessContext context) {
|
||||
}
|
||||
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
package com.jetlumen.ordo.api.exception;
|
||||
|
||||
public final class DefinitionAlreadyExistsException extends OrdoException {
|
||||
public DefinitionAlreadyExistsException(String definitionId) {
|
||||
super("definition already exists: " + definitionId);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.jetlumen.ordo.api.exception;
|
||||
|
||||
public final class DefinitionInUseException extends OrdoException {
|
||||
public DefinitionInUseException(String definitionId) {
|
||||
super("definition in use: " + definitionId);
|
||||
}
|
||||
}
|
||||
+4
@@ -4,4 +4,8 @@ public final class DefinitionNotFoundException extends OrdoException {
|
||||
public DefinitionNotFoundException(String definitionId) {
|
||||
super("definition not found: " + definitionId);
|
||||
}
|
||||
|
||||
public DefinitionNotFoundException(String definitionId, int version) {
|
||||
super("definition not found: " + definitionId + " version " + version);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-12
@@ -6,23 +6,21 @@ import com.jetlumen.ordo.api.query.PageRequest;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/** Storage port for process definitions. */
|
||||
/** Storage port for immutable process definition versions. */
|
||||
public interface ProcessDefinitionRepository {
|
||||
/**
|
||||
* Inserts the definition if no definition with the same id exists.
|
||||
*
|
||||
* @return true if the definition was inserted, false if a definition with the same id already exists
|
||||
* Inserts version 1 when the id is new, returns the latest version when the graph is unchanged,
|
||||
* otherwise inserts {@code latest + 1}.
|
||||
*/
|
||||
boolean insertIfAbsent(ProcessDefinition definition);
|
||||
ProcessDefinition publish(ProcessDefinition definition);
|
||||
|
||||
/**
|
||||
* Inserts the definition, or replaces its name, steps, candidates and transitions
|
||||
* when the id already exists.
|
||||
*/
|
||||
void upsert(ProcessDefinition definition);
|
||||
Optional<ProcessDefinition> findLatest(String definitionId);
|
||||
|
||||
Optional<ProcessDefinition> findById(String definitionId);
|
||||
Optional<ProcessDefinition> find(String definitionId, int version);
|
||||
|
||||
/** Paginated listing of all registered definitions, ordered by id ascending. */
|
||||
/** Paginated listing of the latest version of each definition, ordered by id ascending. */
|
||||
Page<ProcessDefinition> findAll(PageRequest pageRequest);
|
||||
|
||||
/** Paginated versions of one definition, ordered by version descending. */
|
||||
Page<ProcessDefinition> findVersions(String definitionId, PageRequest pageRequest);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,25 @@ class ProcessDefinitionParserTest {
|
||||
));
|
||||
|
||||
assertEquals(expected, ProcessDefinitionParser.fromJson(LEAVE_REQUEST_JSON));
|
||||
assertEquals(0, ProcessDefinitionParser.fromJson(LEAVE_REQUEST_JSON).version());
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoresVersionInJson() {
|
||||
String json = """
|
||||
{
|
||||
"id": "leave",
|
||||
"name": "Leave request",
|
||||
"version": 9,
|
||||
"steps": [
|
||||
{ "id": "manager", "name": "Manager approval", "candidates": ["maria"] }
|
||||
],
|
||||
"transitions": [
|
||||
{ "from": "manager", "to": null }
|
||||
]
|
||||
}
|
||||
""";
|
||||
assertEquals(0, ProcessDefinitionParser.fromJson(json).version());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user