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.
- Start the state machine.
- Send event C to switch state to S211 (4th layer deep).
- Persist the state machine.
- Restore the state machine and send an event associated with state S211 as transition source (D, G or I), not with its ancestors.
- 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:
- While in S211, sending allowed events other than D, G or I works fine.
- While in S11 or S12, sending any allowed events works fine.
- 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 import 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 S21
return TestResponse(...)
}
}
alexshv is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.