alexshv
alexshv

Reputation: 1

Spring state machine persistence does not work correctly

It seems that Spring State Machine persistence does not persist (or restore) a state machine properly if the state machine has at least four layer hierarchy.

I'm using Showcase state machine example to describe the problem.

  1. Start the state machine.
  2. Send event C to switch state to S211 (4th layer deep).
  3. Persist the state machine.
  4. Restore the state machine and send an event associated with state S211 as transition source (D, G or I), not with its ancestors.
  5. Persist again.

Behavior for C: S11 -> S112

> RESTORED <
    [stateContext] stage=TRANSITION_START state=null
    [stateContext] stage=TRANSITION state=null
    [stateContext] stage=STATE_ENTRY state=[S0]
    [stateContext] stage=TRANSITION_START state=[S0]
    [stateContext] stage=TRANSITION state=[S0]
    [stateContext] stage=STATE_ENTRY state=[S0, S1]
    [stateContext] stage=TRANSITION_START state=[S0, S1]
    [stateContext] stage=TRANSITION state=[S0, S1]
    [stateContext] stage=STATE_ENTRY state=[S0, S1, S11]
    [stateContext] stage=STATE_CHANGED state=[S0, S1, S11]
    [stateContext] stage=STATEMACHINE_START state=[S0, S1, S11]
    [stateContext] stage=TRANSITION_END state=[S0, S1, S11]
    [stateContext] stage=STATE_CHANGED state=[S0, S1, S11]
    [stateContext] stage=STATEMACHINE_START state=[S0, S1, S11]
    [stateContext] stage=TRANSITION_END state=[S0, S1, S11]
    [stateContext] stage=STATE_CHANGED state=[S0, S1, S11]
    [stateContext] stage=STATEMACHINE_START state=[S0, S1, S11]
    [stateContext] stage=TRANSITION_END state=[S0, S1, S11]
    [stateContext] stage=STATEMACHINE_STOP state=[S0, S1, S11]
    [stateContext] stage=STATEMACHINE_START state=[S0, S1, S11]
    [stateContext] stage=EVENT_NOT_ACCEPTED state=[S0, S1, S11]
    [stateContext] stage=EVENT_NOT_ACCEPTED state=[S0, S1, S11]
    [stateContext] stage=TRANSITION_START state=[S0, S1, S11]
    [stateContext] stage=TRANSITION state=[S0, S1, S11]
    [stateContext] stage=STATE_EXIT state=[S0, S1, S11]
    [stateContext] stage=STATE_EXIT state=[S0, S1, S11]
    [stateContext] stage=STATEMACHINE_STOP state=[S11]
    [stateContext] stage=STATE_ENTRY state=[S0, S2]
    [stateContext] stage=TRANSITION_START state=[S0, S2]
    [stateContext] stage=TRANSITION state=[S0, S2]
    [stateContext] stage=STATE_ENTRY state=[S0, S2, S21]
    [stateContext] stage=TRANSITION_START state=[S0, S2, S21]
    [stateContext] stage=TRANSITION state=[S0, S2, S21]
    [stateContext] stage=STATE_ENTRY state=[S0, S2, S21, S211]
    [stateContext] stage=STATE_CHANGED state=[S0, S2, S21, S211]
    [stateContext] stage=STATEMACHINE_START state=[S0, S2, S21, S211]
    [stateContext] stage=TRANSITION_END state=[S0, S2, S21, S211]
    [stateContext] stage=STATE_CHANGED state=[S0, S2, S21, S211]
    [stateContext] stage=STATEMACHINE_START state=[S0, S2, S21, S211]
    [stateContext] stage=TRANSITION_END state=[S0, S2, S21, S211]
    [stateContext] stage=STATE_CHANGED state=[S0, S2, S21, S211]
    [stateContext] stage=TRANSITION_END state=[S0, S2, S21, S211]
> PERSISTING... <
> PERSISTED <

Behavior for G: S211 -> S0

> RESTORED <
    [stateContext] stage=TRANSITION_START state=null
    [stateContext] stage=TRANSITION state=null
    [stateContext] stage=STATE_ENTRY state=[S0]
    [stateContext] stage=TRANSITION_START state=[S0]
    [stateContext] stage=TRANSITION state=[S0]
    [stateContext] stage=STATE_ENTRY state=[S0, S1]
    [stateContext] stage=TRANSITION_START state=[S0, S1]
    [stateContext] stage=TRANSITION state=[S0, S1]
    [stateContext] stage=STATE_ENTRY state=[S0, S1, S11]
    [stateContext] stage=STATE_CHANGED state=[S0, S1, S11]
    [stateContext] stage=STATEMACHINE_START state=[S0, S1, S11]
    [stateContext] stage=TRANSITION_END state=[S0, S1, S11]
    [stateContext] stage=STATE_CHANGED state=[S0, S1, S11]
    [stateContext] stage=STATEMACHINE_START state=[S0, S1, S11]
    [stateContext] stage=TRANSITION_END state=[S0, S1, S11]
    [stateContext] stage=STATE_CHANGED state=[S0, S1, S11]
    [stateContext] stage=STATEMACHINE_START state=[S0, S1, S11]
    [stateContext] stage=TRANSITION_END state=[S0, S1, S11]
    [stateContext] stage=STATEMACHINE_STOP state=[S0, S1, S11]
    [stateContext] stage=STATEMACHINE_START state=[S0, S2, S21]
    [stateContext] stage=EVENT_NOT_ACCEPTED state=[S0, S2, S21] <-- there is no S211 anywhere
    [stateContext] stage=EVENT_NOT_ACCEPTED state=[S0, S2, S21]
    [stateContext] stage=EVENT_NOT_ACCEPTED state=[S0, S2, S21]
    [stateContext] stage=EVENT_NOT_ACCEPTED state=[S0, S2, S21]
> PERSISTING... <
java.lang.NullPointerException: Cannot invoke "org.springframework.statemachine.state.State.getId()" because the return value of "org.springframework.statemachine.StateMachine.getState()" is null
    at org.springframework.statemachine.persist.AbstractStateMachinePersister.buildStateMachineContext(AbstractStateMachinePersister.java:84) ~[spring-statemachine-core-4.0.0.jar:4.0.0]
    at org.springframework.statemachine.persist.AbstractStateMachinePersister.buildStateMachineContext(AbstractStateMachinePersister.java:85) ~[spring-statemachine-core-4.0.0.jar:4.0.0]
    at org.springframework.statemachine.persist.AbstractStateMachinePersister.buildStateMachineContext(AbstractStateMachinePersister.java:85) ~[spring-statemachine-core-4.0.0.jar:4.0.0]
    at org.springframework.statemachine.persist.AbstractStateMachinePersister.persist(AbstractStateMachinePersister.java:63) ~[spring-statemachine-core-4.0.0.jar:4.0.0]
    ...

Other details:

  1. While in S211, sending allowed events other than D, G or I works fine.
  2. While in S11 or S12, sending any allowed events works fine.
  3. I think the problem has its origin somewhere during restoration of the state machine as I can't see full state [S0, S2, S21, S211] in the logs after I try to transition from S211 (above).

Spring StateMachine version: 4.0.0

I'm using RedisStateMachineContextRepository as the context repository.

I don't know if it is a bug or it just a misconfiguration. If bug, are there any workarounds, fixes or different persisters that would let me use a similar hierarchy in my project?

I tried to debug the source code but since I'm new to reactive programming, I'm not able to get the root of the problem.

My code if needed

@Configuration
class RedisConfiguration {
    @Bean("redisTemplate")
    fun redisTemplate(jedisConnectionFactory: JedisConnectionFactory): RedisTemplate<String, ByteArray> {
        return RedisTemplate<String, ByteArray>().apply {
            keySerializer = StringRedisSerializer()
            hashKeySerializer = StringRedisSerializer()
            setConnectionFactory(jedisConnectionFactory)
            afterPropertiesSet()
        }
    }
}
@Component
class RedisPersistenceConfiguration {
    @Bean("redisStateMachinePersist")
    fun redisStateMachinePersist(
        @Qualifier("redisTemplate") redisTemplate: RedisTemplate<String, ByteArray>
    ): StateMachinePersist<States, Events, String> {
        val repository = RedisStateMachineContextRepository<States, Events>(redisTemplate.connectionFactory)
        return RepositoryStateMachinePersist(repository)
    }
}
@Configuration
class StateMachinePersister(
    @Qualifier("redisStateMachinePersist")
    private val redisStatesMachinePersist: StateMachinePersist<States, Events, String>
) : RedisStateMachinePersister<States, Events>(redisStatesMachinePersist)
@RestController
class Controller(
    private val statesMachinePersist: StateMachinePersist<States, Events, String>,
    private val statesMachineFactory: StateMachineFactory<States, Events>,
    private val persister: StateMachinePersister
) {
    @PostMapping("/test/{event}")
    fun changeState(@PathVariable event: String): TestResponse {
        val stateMachineId = "test"
        val context = statesMachinePersist.read(stateMachineId)
        val machine = if (context != null) {
            persister.restore(statesMachineFactory.stateMachine, stateMachineId)
        } else {
            statesMachineFactory.getStateMachine(stateMachineId)
        }
machine.sendEvent(Mono.just(MessageBuilder.withPayload(Events.valueOf(event)).build())).blockLast()
        persister.persist(machine, stateMachineId) // <- Exception when trying to send G from S211
        return TestResponse(...)
    }
}

Upvotes: 0

Views: 221

Answers (1)

alexshv
alexshv

Reputation: 1

I implemented my own simple persister and it seems to work fine now.

@Service
class MyStateMachinePersister(
    private val redisTemplate: RedisTemplate<String, ByteArray>,
    private val stateMachineFactory: StateMachineFactory<States, Events>,
    private val kryoSerializationService: KryoStateMachineSerialisationService<States, Events>
) {

    fun persist(stateMachine: StateMachine<States, Events>, stateMachineId: String) {
        val bytes = kryoSerializationService.serialiseStateMachineContext(
            DefaultStateMachineContext(stateMachine.state?.ids?.last(), null, null, stateMachine.extendedState)
        )
        redisTemplate.opsForValue().set(stateMachineId, bytes)
    }

    fun restore(stateMachineId: String): StateMachine<States, Events> {
        val contextValue = redisTemplate.opsForValue().get(stateMachineId)
        val context = kryoSerializationService.deserialiseStateMachineContext(contextValue)
        val sm = stateMachineFactory.getStateMachine(stateMachineId)
        sm.stopReactively().block()
        sm.stateMachineAccessor.doWithAllRegions { sma ->
            sma.resetStateMachineReactively(context).block()
        }
        sm.startReactively().block()
        return sm
    }

    fun persisted(stateMachineId: String): Boolean {
        return redisTemplate.opsForValue().get(stateMachineId) != null
    }
}

Upvotes: 0

Related Questions