26. Persisting State Machine

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]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.

26.1 Using StateMachineContext

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.

26.2 Using StateMachinePersister

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]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"));

26.3 Using Redis

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]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.