Reputation: 1
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.
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:
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
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