spring-projects / spring-statemachine

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

StateMachineRuntimePersister doesn't save/restore region states #803

Open holyhoehle opened 5 years ago

holyhoehle commented 5 years ago

I have the following StateMachine configuration:

@Configuration
open class PooledAppConfig {

    @Bean
    fun stateMachineRuntimePersister(
            jpaStateMachineRepository: JpaStateMachineRepository): StateMachineRuntimePersister<String, String, String> {
        return JpaPersistingStateMachineInterceptor<String, String, String>(jpaStateMachineRepository)
    }

    @Bean
    fun stateMachineTarget(stateMachineRuntimePersister: StateMachineRuntimePersister<String, String, String>): StateMachine<String, String> {
        val builder = StateMachineBuilder.builder<String, String>()
        builder.configureConfiguration()
                .withConfiguration()
                .machineId("ItemSM")

        builder.configureConfiguration()
                .withPersistence()
                .runtimePersister(stateMachineRuntimePersister)

        builder.configureStates()
                .withStates()
                .initial("Init")
                .state("S1")
                .fork("FRK")
                .state("GROUP")
                .join("JN")
                .and()
                .withStates()
                    .parent("GROUP")
                    .region("R1")
                    .initial("F1I")
                    .state("F1S")
                    .end("F1E")
                .and()
                .withStates()
                    .parent("GROUP")
                    .region("R2")
                    .initial("F2I")
                    .state("F2S")
                    .end("F2E")
                .and()
                .withStates()
                    .parent("GROUP")
                    .region("R3")
                    .initial("F3I")
                    .state("F3S")
                    .end("F3E")
                .and()
                .withStates()
                .state("END")

        builder.configureTransitions()
                .withExternal()
                    .source("Init").target("S1")
                    .event("E_IS1")
                    .and()
                .withExternal()
                    .source("S1").target("FRK")
                    .event("E_FRK")
                    .and()
                .withFork()
                    .source("FRK")
                    .target("GROUP")
                    .and()
                .withJoin()
                    .source("GROUP")
                    .target("JN")
                    .and()
                .withExternal()
                    .source("F1I").target("F1S")
                    .event("E_F1IS")
                    .and()
                .withExternal()
                    .source("F2I").target("F2S")
                    .event("E_F2IS")
                    .and()
                .withExternal()
                    .source("F3I").target("F3S")
                    .event("E_F3IS")
                    .and()
                .withExternal()
                    .source("F1S").target("F1E")
                    .event("E_F1SE")
                    .and()
                .withExternal()
                    .source("F2S").target("F2E")
                    .event("E_F2SE")
                    .and()
                .withExternal()
                    .source("F3S").target("F3E")
                    .event("E_F3SE")
                    .and()
                .withExternal()
                    .source("JN").target("END")

        return builder.build()
    }
}

And my controller looks like this:

@RestController
class StateMachineController {

    @Autowired
    private lateinit var stateMachine: StateMachine<String, String>

    @Autowired
    private lateinit var runtimePersister: StateMachineRuntimePersister<String, String, String>

    @GetMapping("/state")
    fun state(String, @RequestParam("event") event: String): String {
        resetStateMachineFromStore()
        feedStateMachine(event)
        return stateMachine.state.toString()
    }

    private fun resetStateMachineFromStore() {
        val context = runtimePersister.read("ItemSM")
        if (context != null) {
            stateMachine.stop()
            stateMachine.stateMachineAccessor.doWithRegion {
                it.resetStateMachine(context)
            }
            stateMachine.start()
        }
    }

    private fun feedStateMachine(event: String) {
        stateMachine.sendEvent(event)
    }
}

If I trigger the events in the following order the following SQL statements are being executed:

'E_IS1' -> UPDATE STATE_MACHINE SET STATE='S1', [...] WHERE MACHINE_ID='ItemSM' 

'E_FRK' -> UPDATE STATE_MACHINE SET STATE='GROUP', [...] WHERE MACHINE_ID='ItemSM'
          -> UPDATE STATE_MACHINE SET STATE='F1I', [...] WHERE MACHINE_ID='ItemSM#R1'
          -> UPDATE STATE_MACHINE SET STATE='F3I', [...] WHERE MACHINE_ID='ItemSM#R3'
          -> UPDATE STATE_MACHINE SET STATE='F2I', [...] WHERE MACHINE_ID='ItemSM#R2'

'E_F1IS' -> UPDATE STATE_MACHINE SET STATE='F1S', [...] WHERE MACHINE_ID='ItemSM'
'E_F2IS' -> UPDATE STATE_MACHINE SET STATE='F2S', [...] WHERE MACHINE_ID='ItemSM'

For the last two events (E_F1IS/E_F2IS) I would have expected the region rows ItemSM#R1 and ItemSM#R2 to get updated to F1S and F2S. Instead the main ItemSM row gets updated twice.

This leads to the problem that the state machine cannot be restored properly. The resulting state machine is loaded with state GROUP but the region states are all in the initial F1I,F2I,F3I states.

Is there a problem with my state machine config or what is the reason for this behavior?

michelsciortino commented 3 years ago

I have the exact same problem in version 3.0.1.

Any news about this?