Traditionally an instance of a state machine is used as is within a running program. More dynamic behaviour is possible to achieve via dynamic builders and factories which allows state machine instantiation on-demand. Building an instance of a state machine is relatively heavy operation so if there is a need to i.e. handle arbitrary state change in a database using a state machine we need to find a better and faster way to do it.
Persist feature allows user to save a state of a state machine itself into an external repository and later reset a state machine based of serialized state. For example if you have a database table keeping orders it would be way too expensive to update order state via a state machine if a new instance would need to be build for every change. Persist feature allows you to reset a state machine state without instantiating a new state machine instance.
Note | |
---|---|
There is one recipe Chapter 33, Persist and one sample Chapter 40, Persist which provides more info about persisting states. |
While it is possible to build a custom persistence feature using a
StateMachineListener
it has one conceptual problem. When listener
notifies a change of state, state change has already happened. If a
custom persistent method within a listener fails to update serialized
state in an external repository, state in a state machine and state in
an external repository are then in inconsistent state.
State machine interceptor can be used instead of where attempt to save serialized state into an external storage is done during the a state change within a state machine. If this interceptor callback fails, state change attempt will be halted and instead of ending into an inconsistent state, user can then handle this error manually. Using the interceptors are discussed in Chapter 23, State Machine Interceptor.
It is impossible to persist a StateMachine using normal java
serialization as object graph is too rich and contains too much
dependencies into other Spring context classes. StateMachineContext
is a runtime representation of a state machine which can be used to
restore an existing machine into a state represented by a particular
StateMachineContext
object.
Building a StateMachineContext
and then restoring a state machine
from it has always been a little bit of a black magic if done
manually. Interface StateMachinePersister
aims to ease these
operations by providing persist and restore methods. Default
implementation of this interface is DefaultStateMachinePersister
Usage of a StateMachinePersister
is easy to demonstrate by following
a snippets from tests. We start by creating to two similar configs for
a state machine machine1
and machine2
. We could build different
machines for this demonstration using various other ways but this
serves a purpose for this case.
@Configuration @EnableStateMachine(name = "machine1") static class Config1 extends Config { } @Configuration @EnableStateMachine(name = "machine2") static class Config2 extends Config { } static class Config extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineStateConfigurer<String, String> states) throws Exception { states .withStates() .initial("S1") .state("S1") .state("S2"); } @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions .withExternal() .source("S1") .target("S2") .event("E1"); } }
As we’re using a StateMachinePersist
we simply create an in-memory
implementation.
Note | |
---|---|
In-memory sample is just for demonstration purposes, use a real persistent storage implementations. |
static class InMemoryStateMachinePersist implements StateMachinePersist<String, String, String> { private final HashMap<String, StateMachineContext<String, String>> contexts = new HashMap<>(); @Override public void write(StateMachineContext<String, String> context, String contextObj) throws Exception { contexts.put(contextObj, context); } @Override public StateMachineContext<String, String> read(String contextObj) throws Exception { return contexts.get(contextObj); } }
After we have instantiated two different machines we can transfer
machine1
into state S2
via event E1
, then persist it and restore
machine2
.
InMemoryStateMachinePersist stateMachinePersist = new InMemoryStateMachinePersist(); StateMachinePersister<String, String, String> persister = new DefaultStateMachinePersister<>(stateMachinePersist); StateMachine<String, String> stateMachine1 = context.getBean("machine1", StateMachine.class); StateMachine<String, String> stateMachine2 = context.getBean("machine2", StateMachine.class); stateMachine1.start(); stateMachine1.sendEvent("E1"); assertThat(stateMachine1.getState().getIds(), contains("S2")); persister.persist(stateMachine1, "myid"); persister.restore(stateMachine2, "myid"); assertThat(stateMachine2.getState().getIds(), contains("S2"));
Support for persisting State Machine into Redis is done via
RepositoryStateMachinePersist
which implements
StateMachinePersist
. Specific implementation is a
RedisStateMachineContextRepository
which uses kryo
serialization to
persist a StateMachineContext
into Redis
.
For StateMachinePersister
we have a redis related
RedisStateMachinePersister
implementation which takes an instance of
a StateMachinePersist
and uses String as its context object.
Tip | |
---|---|
Check sample Chapter 45, Event Service for detailed usage. |
RedisStateMachineContextRepository
will need a
RedisConnectionFactory
for it to work and we recommend a
JedisConnectionFactory
for it as seeing from above example.