spring-projects / spring-statemachine

Spring Statemachine is a framework for application developers to use state machine concepts with Spring.
1.54k stars 607 forks source link

Parallel submachines break exitActions #1167

Open wlfbck opened 1 day ago

wlfbck commented 1 day ago

If you attach more then one submachine to a state using .parent("XYZ") it will break the exit actions of these submachines. See example below. This bug is hinted at by https://github.com/spring-projects/spring-statemachine/issues/969 but does not include the correct reason why.

Unfortunately i don't have any fancy graphical tool, so you get some excalidraw from me.

This one breaks the state machine: grafik

This one is fine: grafik

public class Testy {
    private static final Logger logger = LoggerFactory.getLogger(Testy.class);

    public static void main(String[] args) throws Exception {
        StateMachineBuilder.Builder<String, String> builder = StateMachineBuilder.builder();
        builder.configureConfiguration().withConfiguration()
                .regionExecutionPolicy(RegionExecutionPolicy.SEQUENTIAL)
                .transitionConflictPolicy(TransitionConflictPolicy.PARENT)
                .listener(new StateMachineListenerAdapter<>() {
                    @Override
                    public void stateChanged(org.springframework.statemachine.state.State<String, String> from, org.springframework.statemachine.state.State<String, String> to) {
                        logger.info("from \n{} \nto \n{}", from, to);
                    }

                    @Override
                    public void eventNotAccepted(Message<String> event) {
                        //logger.info("event not accepted: {}", event);
                    }

                    @Override
                    public void transition(Transition<String, String> transition) {
                        logger.info("transition: {}", transition);
                    }
                })
                .machineId("GD24-machine");

        builder.configureStates().withStates()
                .initial("order-canExist")
                .state("order-Exists", ctx -> logger.info("hello order"), ctx -> logger.info("goodbye order"))
                .and().withStates()
                .parent("order-Exists")
                .initial("ncprogram-canExist")
                .state("ncprogram-Exists", ctx -> logger.info("hello ncprog"), ctx -> logger.info("goodbye ncprog"))
                .and().withStates()
                // change to "ncprogram-Exists" and it will magically work
                .parent("order-Exists")
                .initial("madeup-canExist")
                .state("madeup-Exists", ctx -> logger.info("hello madeup"), ctx -> logger.info("goodbye madeup"));

        builder.configureTransitions()
                .withExternal().source("order-canExist").target("order-Exists")
                .event("order-start")
                .and().withExternal().source("order-Exists").target("order-canExist")
                .event("order-end")
                .and().withExternal().source("ncprogram-canExist").target("ncprogram-Exists")
                .event("ncprogram-start")
                .and().withExternal().source("madeup-canExist").target("madeup-Exists")
                .event("madeup-start");

        StateMachine<String, String> stateMachine = builder.build();

        stateMachine.startReactively().subscribe();

        logger.info("sending ncprog start");
        Message<String> message1 = MessageBuilder.withPayload("ncprogram-start").build();
        stateMachine.sendEvent(Mono.just(message1)).subscribe();
        logger.info("sending order start");
        Message<String> message2 = MessageBuilder.withPayload("order-start").build();
        stateMachine.sendEvent(Mono.just(message2)).subscribe();
        logger.info("sending ncprog start");
        Message<String> message3 = MessageBuilder.withPayload("ncprogram-start").build();
        stateMachine.sendEvent(Mono.just(message3)).subscribe();
        logger.info("sending madeup start");
        Message<String> message4 = MessageBuilder.withPayload("madeup-start").build();
        stateMachine.sendEvent(Mono.just(message4)).subscribe();
        logger.info("sending order end");
        Message<String> message5 = MessageBuilder.withPayload("order-end").build();
        stateMachine.sendEvent(Mono.just(message5)).subscribe();
    }
}

Expected output from first example would be:

15:40:03,713 INFO  com.plansee.edge.flink.Testy                                 [] - goodbye madeup
15:40:03,720 INFO  com.plansee.edge.flink.Testy                                 [] - goodbye ncprog
15:40:03,720 INFO  com.plansee.edge.flink.Testy                                 [] - goodbye order

Instead, only this comes:

15:40:35,452 INFO  com.plansee.edge.flink.Testy                                 [] - goodbye order

I assume this is happening because the variant with 2 submachines is triggering a RegionState to ObjectState transition, while with 1 submachine it is triggering a StateMachineState to ObjectState

wlfbck commented 1 day ago

Looking at the debug output some more, it seems that while building the statemachine using the same parent twice turns the submachines into regions. That seems wrong to me.