Gate shared-service multi-attach to provable shared infrastructure.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -62,11 +62,13 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TriggerPoint trigger = chain.getTriggerPoint();
|
TriggerPoint trigger = chain.getTriggerPoint();
|
||||||
if (trigger != null && matchesConfiguredTransitionEvent(trigger, machineTransitions)) {
|
if (trigger != null
|
||||||
|
&& SharedServiceRoutingPolicy.isProvablySharedInfrastructure(chain)
|
||||||
|
&& matchesConfiguredTransitionEvent(trigger, machineTransitions)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return isRoutedToCorrectMachine(chain, currentMachineName, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -9,9 +9,44 @@ import java.util.Set;
|
|||||||
*/
|
*/
|
||||||
final class PackageNameRoutingHeuristics {
|
final class PackageNameRoutingHeuristics {
|
||||||
|
|
||||||
|
private static final Set<String> GENERIC_CLASS_PREFIXES = Set.of(
|
||||||
|
"state", "statemachine", "config", "configuration", "adapter",
|
||||||
|
"enterprise", "app", "global", "core", "project", "main",
|
||||||
|
"base", "common", "shared");
|
||||||
|
|
||||||
|
private static final Set<String> GENERIC_PACKAGE_SEGMENTS = Set.of(
|
||||||
|
"config", "configuration", "service", "services", "impl", "api", "web",
|
||||||
|
"controller", "controllers", "messaging", "listener", "listeners",
|
||||||
|
"repository", "repositories", "db", "model", "models", "domain",
|
||||||
|
"constants", "utils", "helper", "helpers", "event", "events",
|
||||||
|
"state", "states", "transition", "transitions", "shared", "common",
|
||||||
|
"global", "core", "base", "main", "app", "enterprise");
|
||||||
|
|
||||||
private PackageNameRoutingHeuristics() {
|
private PackageNameRoutingHeuristics() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true when any class in the call chain carries a vertical domain prefix or package segment
|
||||||
|
*/
|
||||||
|
static boolean hasVerticalDomainOwnership(CallChain chain) {
|
||||||
|
if (chain == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (chain.getTriggerPoint() != null && chain.getTriggerPoint().getClassName() != null) {
|
||||||
|
if (classHasVerticalDomainOwnership(chain.getTriggerPoint().getClassName())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chain.getMethodChain() != null) {
|
||||||
|
for (String method : chain.getMethodChain()) {
|
||||||
|
if (classHasVerticalDomainOwnership(getClassNameOnly(method))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return true/false if package heuristics decide, null if inconclusive
|
* @return true/false if package heuristics decide, null if inconclusive
|
||||||
*/
|
*/
|
||||||
@@ -60,7 +95,7 @@ final class PackageNameRoutingHeuristics {
|
|||||||
String chainSimple = getSimpleClassName(chainClass);
|
String chainSimple = getSimpleClassName(chainClass);
|
||||||
String chainPrefix = getFirstCamelCaseWord(chainSimple);
|
String chainPrefix = getFirstCamelCaseWord(chainSimple);
|
||||||
|
|
||||||
if (smPrefix != null && chainPrefix != null && smPrefix.equals(chainPrefix)) {
|
if (hasMatchingDomainPrefix(smPrefix, chainPrefix)) {
|
||||||
hasPositiveMatch = true;
|
hasPositiveMatch = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +110,7 @@ final class PackageNameRoutingHeuristics {
|
|||||||
String chainSimple = getSimpleClassName(chainClass);
|
String chainSimple = getSimpleClassName(chainClass);
|
||||||
String chainPrefix = getFirstCamelCaseWord(chainSimple);
|
String chainPrefix = getFirstCamelCaseWord(chainSimple);
|
||||||
|
|
||||||
if (smPrefix != null && chainPrefix != null && smPrefix.equals(chainPrefix)) {
|
if (hasMatchingDomainPrefix(smPrefix, chainPrefix)) {
|
||||||
hasPositiveMatch = true;
|
hasPositiveMatch = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,16 +186,7 @@ final class PackageNameRoutingHeuristics {
|
|||||||
String smLower = smPrefix.toLowerCase();
|
String smLower = smPrefix.toLowerCase();
|
||||||
String chainLower = chainPrefix.toLowerCase();
|
String chainLower = chainPrefix.toLowerCase();
|
||||||
|
|
||||||
if (smLower.equals("state") || smLower.equals("statemachine") || smLower.equals("config")
|
if (isGenericClassPrefix(smLower) || isGenericClassPrefix(chainLower)) {
|
||||||
|| smLower.equals("configuration") || smLower.equals("adapter")
|
|
||||||
|| smLower.equals("enterprise") || smLower.equals("app") || smLower.equals("global")
|
|
||||||
|| smLower.equals("core") || smLower.equals("project") || smLower.equals("main")
|
|
||||||
|| smLower.equals("base") || smLower.equals("common") || smLower.equals("shared")
|
|
||||||
|| chainLower.equals("state") || chainLower.equals("statemachine") || chainLower.equals("config")
|
|
||||||
|| chainLower.equals("configuration") || chainLower.equals("adapter")
|
|
||||||
|| chainLower.equals("enterprise") || chainLower.equals("app") || chainLower.equals("global")
|
|
||||||
|| chainLower.equals("core") || chainLower.equals("project") || chainLower.equals("main")
|
|
||||||
|| chainLower.equals("base") || chainLower.equals("common") || chainLower.equals("shared")) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,22 +207,15 @@ final class PackageNameRoutingHeuristics {
|
|||||||
}
|
}
|
||||||
if (matchIdx >= 1) {
|
if (matchIdx >= 1) {
|
||||||
boolean onlyGenericDivergence = true;
|
boolean onlyGenericDivergence = true;
|
||||||
Set<String> genericLayers = Set.of(
|
|
||||||
"config", "configuration", "service", "services", "impl", "api", "web",
|
|
||||||
"controller", "controllers", "messaging", "listener", "listeners",
|
|
||||||
"repository", "repositories", "db", "model", "models", "domain",
|
|
||||||
"constants", "utils", "helper", "helpers", "event", "events",
|
|
||||||
"state", "states", "transition", "transitions"
|
|
||||||
);
|
|
||||||
for (int i = matchIdx + 1; i < p1.length; i++) {
|
for (int i = matchIdx + 1; i < p1.length; i++) {
|
||||||
if (!genericLayers.contains(p1[i].toLowerCase())) {
|
if (!isGenericPackageSegment(p1[i])) {
|
||||||
onlyGenericDivergence = false;
|
onlyGenericDivergence = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (onlyGenericDivergence) {
|
if (onlyGenericDivergence) {
|
||||||
for (int i = matchIdx + 1; i < p2.length; i++) {
|
for (int i = matchIdx + 1; i < p2.length; i++) {
|
||||||
if (!genericLayers.contains(p2[i].toLowerCase())) {
|
if (!isGenericPackageSegment(p2[i])) {
|
||||||
onlyGenericDivergence = false;
|
onlyGenericDivergence = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -253,4 +272,40 @@ final class PackageNameRoutingHeuristics {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean classHasVerticalDomainOwnership(String classFqn) {
|
||||||
|
if (classFqn == null || classFqn.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String prefix = getFirstCamelCaseWord(getSimpleClassName(classFqn));
|
||||||
|
if (prefix != null && !isGenericClassPrefix(prefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return hasVerticalPackageSegment(getPackageName(classFqn));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasVerticalPackageSegment(String packageName) {
|
||||||
|
if (packageName == null || packageName.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String[] segments = packageName.split("\\.");
|
||||||
|
for (int i = 2; i < segments.length; i++) {
|
||||||
|
if (!isGenericPackageSegment(segments[i])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasMatchingDomainPrefix(String smPrefix, String chainPrefix) {
|
||||||
|
return smPrefix != null && chainPrefix != null && smPrefix.equals(chainPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isGenericClassPrefix(String prefix) {
|
||||||
|
return prefix != null && GENERIC_CLASS_PREFIXES.contains(prefix.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isGenericPackageSegment(String segment) {
|
||||||
|
return segment != null && GENERIC_PACKAGE_SEGMENTS.contains(segment.toLowerCase());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decides whether a call chain is provably shared infrastructure that may intentionally
|
||||||
|
* attach to multiple state machines when they share the same transition event.
|
||||||
|
* <p>
|
||||||
|
* Shared infrastructure requires source-derived evidence only: no distinct machine enum types
|
||||||
|
* at the sendEvent site and no vertical domain ownership in the call chain classes/packages.
|
||||||
|
* Event-name matching alone is never sufficient outside this policy.
|
||||||
|
*/
|
||||||
|
final class SharedServiceRoutingPolicy {
|
||||||
|
|
||||||
|
private SharedServiceRoutingPolicy() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true when the chain may multi-attach to every machine that shares its transition event
|
||||||
|
*/
|
||||||
|
static boolean isProvablySharedInfrastructure(CallChain chain) {
|
||||||
|
if (chain == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
MachineRoutingEvidence evidence = MachineRoutingEvidence.from(chain);
|
||||||
|
if (evidence.eventTypeFqn() != null || evidence.stateTypeFqn() != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !PackageNameRoutingHeuristics.hasVerticalDomainOwnership(chain);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.enricher.routing;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Event;
|
||||||
|
import click.kamil.springstatemachineexporter.model.State;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class SharedServiceRoutingPolicyTest {
|
||||||
|
|
||||||
|
private final HeuristicBeanResolutionEngine engine = new HeuristicBeanResolutionEngine();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void commonOrderServiceChainIsProvablySharedInfrastructure() {
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder().event("PROCESS").build())
|
||||||
|
.methodChain(List.of(
|
||||||
|
"com.example.CommonOrderController.post()",
|
||||||
|
"com.example.CommonOrderService.processOrderEvent()"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThat(SharedServiceRoutingPolicy.isProvablySharedInfrastructure(chain)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void electronicsVerticalChainIsNotProvablySharedInfrastructure() {
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
|
||||||
|
.methodChain(List.of(
|
||||||
|
"com.example.electronics.ElectronicsOrderController.post()",
|
||||||
|
"com.example.electronics.ElectronicsOrderService.processOrderEvent()"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThat(SharedServiceRoutingPolicy.isProvablySharedInfrastructure(chain)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void chainWithProvenTypeFqnsIsNotProvablySharedInfrastructure() {
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder()
|
||||||
|
.event("com.example.OrderEvent.PAY")
|
||||||
|
.eventTypeFqn("com.example.OrderEvent")
|
||||||
|
.stateTypeFqn("com.example.OrderState")
|
||||||
|
.build())
|
||||||
|
.methodChain(List.of("com.example.SharedDispatcher.payOrder()"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThat(SharedServiceRoutingPolicy.isProvablySharedInfrastructure(chain)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sharedInfrastructureMayMultiAttachOnSharedEventInMultiMachineContext(@TempDir Path tempDir) throws Exception {
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), """
|
||||||
|
package com.example;
|
||||||
|
@org.springframework.statemachine.config.EnableStateMachine
|
||||||
|
public class OrderStateMachineConfig
|
||||||
|
extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<String, String> {}
|
||||||
|
""");
|
||||||
|
Files.writeString(tempDir.resolve("DocumentConfig.java"), """
|
||||||
|
package com.example;
|
||||||
|
@org.springframework.statemachine.config.EnableStateMachine
|
||||||
|
public class DocumentStateMachineConfig
|
||||||
|
extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<String, String> {}
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder().event("PROCESS").build())
|
||||||
|
.methodChain(List.of(
|
||||||
|
"com.example.CommonOrderController.post()",
|
||||||
|
"com.example.CommonOrderService.processOrderEvent()"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Transition transition = new Transition();
|
||||||
|
transition.setSourceStates(List.of(State.of("NEW", "NEW")));
|
||||||
|
transition.setTargetStates(List.of(State.of("PROCESSED", "PROCESSED")));
|
||||||
|
transition.setEvent(Event.of("PROCESS", "PROCESS"));
|
||||||
|
List<Transition> machineTransitions = List.of(transition);
|
||||||
|
|
||||||
|
assertThat(engine.hasProvenMachineAffinity(
|
||||||
|
chain, "com.example.OrderStateMachineConfig", context, machineTransitions)).isTrue();
|
||||||
|
assertThat(engine.hasProvenMachineAffinity(
|
||||||
|
chain, "com.example.DocumentStateMachineConfig", context, machineTransitions)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void verticalChainFailsClosedOnEventMatchInMultiMachineContext(@TempDir Path tempDir) throws Exception {
|
||||||
|
Files.writeString(tempDir.resolve("ElectronicsConfig.java"), """
|
||||||
|
package com.example.electronics;
|
||||||
|
@org.springframework.statemachine.config.EnableStateMachine
|
||||||
|
public class ElectronicsStateMachineConfig
|
||||||
|
extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<String, String> {}
|
||||||
|
""");
|
||||||
|
Files.writeString(tempDir.resolve("ComputerConfig.java"), """
|
||||||
|
package com.example.computerstore;
|
||||||
|
@org.springframework.statemachine.config.EnableStateMachine
|
||||||
|
public class ComputerStateMachineConfig
|
||||||
|
extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<String, String> {}
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
|
||||||
|
.methodChain(List.of(
|
||||||
|
"com.example.electronics.ElectronicsOrderController.post()",
|
||||||
|
"com.example.electronics.ElectronicsOrderService.processOrderEvent()"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Transition transition = new Transition();
|
||||||
|
transition.setSourceStates(List.of(State.of("NEW", "NEW")));
|
||||||
|
transition.setTargetStates(List.of(State.of("ASSEMBLED", "ASSEMBLED")));
|
||||||
|
transition.setEvent(Event.of("ASSEMBLE", "ASSEMBLE"));
|
||||||
|
List<Transition> machineTransitions = List.of(transition);
|
||||||
|
|
||||||
|
assertThat(engine.hasProvenMachineAffinity(
|
||||||
|
chain, "com.example.electronics.ElectronicsStateMachineConfig", context, machineTransitions))
|
||||||
|
.isTrue();
|
||||||
|
assertThat(engine.hasProvenMachineAffinity(
|
||||||
|
chain, "com.example.computerstore.ComputerStateMachineConfig", context, machineTransitions))
|
||||||
|
.isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void provenDistinctTypesStillRouteToSingleMachine(@TempDir Path tempDir) throws Exception {
|
||||||
|
Files.writeString(tempDir.resolve("SharedDispatcher.java"), """
|
||||||
|
package com.example;
|
||||||
|
public class SharedDispatcher {
|
||||||
|
void payOrder() {
|
||||||
|
org.springframework.statemachine.StateMachine<OrderState, OrderEvent> machine = null;
|
||||||
|
machine.sendEvent(OrderEvent.PAY);
|
||||||
|
}
|
||||||
|
void submitDocument() {
|
||||||
|
org.springframework.statemachine.StateMachine<DocumentState, DocumentEvent> machine = null;
|
||||||
|
machine.sendEvent(DocumentEvent.SUBMIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enum OrderEvent { PAY }
|
||||||
|
enum OrderState { NEW, PAID }
|
||||||
|
enum DocumentEvent { SUBMIT }
|
||||||
|
enum DocumentState { DRAFT }
|
||||||
|
class OrderStateMachineConfiguration
|
||||||
|
extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<OrderState, OrderEvent> {}
|
||||||
|
class DocumentStateMachineConfiguration
|
||||||
|
extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<DocumentState, DocumentEvent> {}
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
CallChain orderChain = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder()
|
||||||
|
.event("com.example.OrderEvent.PAY")
|
||||||
|
.eventTypeFqn("com.example.OrderEvent")
|
||||||
|
.stateTypeFqn("com.example.OrderState")
|
||||||
|
.className("com.example.SharedDispatcher")
|
||||||
|
.methodName("payOrder")
|
||||||
|
.build())
|
||||||
|
.methodChain(List.of("com.example.SharedDispatcher.payOrder()"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Transition payTransition = new Transition();
|
||||||
|
payTransition.setSourceStates(List.of(State.of("NEW", "NEW")));
|
||||||
|
payTransition.setTargetStates(List.of(State.of("PAID", "PAID")));
|
||||||
|
payTransition.setEvent(Event.of("PAY", "PAY"));
|
||||||
|
|
||||||
|
assertThat(engine.hasProvenMachineAffinity(
|
||||||
|
orderChain, "com.example.OrderStateMachineConfiguration", context, List.of(payTransition)))
|
||||||
|
.isTrue();
|
||||||
|
assertThat(engine.hasProvenMachineAffinity(
|
||||||
|
orderChain, "com.example.DocumentStateMachineConfiguration", context, List.of(payTransition)))
|
||||||
|
.isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user