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
@@ -3,7 +3,6 @@ package com.jetlumen.ordo.spring;
import com.jetlumen.ordo.api.OrdoEngine;
import com.jetlumen.ordo.api.ProcessDefinition;
import com.jetlumen.ordo.api.ProcessDefinitionParser;
import com.jetlumen.ordo.api.exception.DefinitionInUseException;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.io.Resource;
@@ -13,7 +12,7 @@ import org.springframework.core.io.support.ResourcePatternResolver;
import java.io.InputStream;
import java.util.Objects;
/** Loads structural JSON process definitions and upserts them when no instance is running. */
/** Loads structural JSON process definitions and publishes a new version when the graph changed. */
public final class OrdoDefinitionLoader implements ApplicationRunner {
private final OrdoEngine engine;
private final String location;
@@ -49,11 +48,7 @@ public final class OrdoDefinitionLoader implements ApplicationRunner {
} catch (RuntimeException e) {
throw new IllegalStateException("failed to parse ordo definition from " + describe(resource), e);
}
try {
engine.replace(definition);
} catch (DefinitionInUseException ignored) {
// keep the stored graph while RUNNING instances still reference it
}
engine.publish(definition);
}
}
@@ -49,7 +49,7 @@ class OrdoJdbcAutoConfigurationTest {
assertThat(context).hasSingleBean(OrdoEngine.class);
OrdoEngine engine = context.getBean(OrdoEngine.class);
engine.register(LEAVE_REQUEST);
engine.publish(LEAVE_REQUEST);
ProcessInstance instance = engine.start("leave-request", "alice");
List<ApprovalTask> pending = engine.findPendingTasksByAssignee("maria");
assertThat(pending).hasSize(1);
@@ -92,8 +92,9 @@ class OrdoJdbcAutoConfigurationTest {
loader.load();
ProcessDefinition definition = context.getBean(ProcessDefinitionRepository.class)
.findById("leave-request-routed")
.findLatest("leave-request-routed")
.orElseThrow();
assertThat(definition.version()).isEqualTo(1);
assertThat(definition.name()).isEqualTo("Leave request");
assertThat(definition.steps()).hasSize(2);
assertThat(definition.transitions()).hasSize(3);
@@ -104,13 +105,13 @@ class OrdoJdbcAutoConfigurationTest {
void reloadsClasspathJsonDefinitionsWhenNoInstanceIsRunning() {
withDataSourceRunner.run(context -> {
OrdoEngine engine = context.getBean(OrdoEngine.class);
engine.replace(ProcessDefinition.linear("leave-request-routed", "stale", List.of(
engine.publish(ProcessDefinition.linear("leave-request-routed", "stale", List.of(
ApprovalStep.single("lead", "Lead approval", "lee"))));
context.getBean(OrdoDefinitionLoader.class).load();
ProcessDefinition definition = context.getBean(ProcessDefinitionRepository.class)
.findById("leave-request-routed")
.findLatest("leave-request-routed")
.orElseThrow();
assertThat(definition.name()).isEqualTo("Leave request");
assertThat(definition.steps()).hasSize(2);
@@ -118,20 +119,23 @@ class OrdoJdbcAutoConfigurationTest {
}
@Test
void keepsStoredDefinitionWhenReloadFindsRunningInstances() {
void publishesClasspathJsonEvenWhenInstancesAreRunning() {
withDataSourceRunner.run(context -> {
OrdoEngine engine = context.getBean(OrdoEngine.class);
engine.replace(ProcessDefinition.linear("leave-request-routed", "stale", List.of(
engine.publish(ProcessDefinition.linear("leave-request-routed", "stale", List.of(
ApprovalStep.single("lead", "Lead approval", "lee"))));
engine.start("leave-request-routed", "alice");
ProcessInstance instance = engine.start("leave-request-routed", "alice");
context.getBean(OrdoDefinitionLoader.class).load();
ProcessDefinition definition = context.getBean(ProcessDefinitionRepository.class)
.findById("leave-request-routed")
.findLatest("leave-request-routed")
.orElseThrow();
assertThat(definition.name()).isEqualTo("stale");
assertThat(definition.steps()).hasSize(1);
assertThat(definition.name()).isEqualTo("Leave request");
assertThat(definition.steps()).hasSize(2);
assertThat(instance.definitionVersion()).isEqualTo(engine.findInstance(instance.id()).orElseThrow()
.definitionVersion());
assertThat(engine.findPendingTasksByInstanceId(instance.id()).get(0).stepId()).isEqualTo("lead");
});
}
@@ -160,7 +164,7 @@ class OrdoJdbcAutoConfigurationTest {
withDataSourceRunner.withUserConfiguration(RecordingListenerConfig.class)
.run(context -> {
OrdoEngine engine = context.getBean(OrdoEngine.class);
engine.register(LEAVE_REQUEST);
engine.publish(LEAVE_REQUEST);
ProcessInstance instance = engine.start("leave-request", "alice");
engine.approve(engine.findPendingTasksByAssignee("maria").get(0).id(), "maria");