50. Data Persist

Data Persist is an example how state machine concepts can be used with persisting machine in an external repository. This sample is using embedded H2 database with a H2 Console to ease playing with a database. Optionally it’s also possible to enable Redis or MongoDB.

This sample uses spring-statemachine-boot which on default auto-configures repositories and entity classes needed for JPA. Thus only @SpringBootApplication is needed.

@SpringBootApplication
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

StateMachineRuntimePersister is a new interface working on a runtime level of a StateMachine and its implementation JpaPersistingStateMachineInterceptor is meant to be used with a JPA.

@Configuration
@Profile("jpa")
public static class JpaPersisterConfig {

    @Bean
    public StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister(
            JpaStateMachineRepository jpaStateMachineRepository) {
        return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
    }
}

Same configuration optionally enabled with mongo profile.

@Configuration
@Profile("mongo")
public static class MongoPersisterConfig {

    @Bean
    public StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister(
            MongoDbStateMachineRepository jpaStateMachineRepository) {
        return new MongoDbPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
    }
}

Same configuration optionally enabled with redis profile.

@Configuration
@Profile("redis")
public static class RedisPersisterConfig {

    @Bean
    public StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister(
            RedisStateMachineRepository jpaStateMachineRepository) {
        return new RedisPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
    }
}

StateMachine can be configured to use runtime persistence by using withPersistence config method.

@Autowired
private StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister;

@Override
public void configure(StateMachineConfigurationConfigurer<States, Events> config)
        throws Exception {
    config
        .withPersistence()
            .runtimePersister(stateMachineRuntimePersister);
}

In this sample we also use DefaultStateMachineService which makes it easier to work with multiple machines

@Bean
public StateMachineService<States, Events> stateMachineService(
        StateMachineFactory<States, Events> stateMachineFactory,
        StateMachineRuntimePersister<States, Events, String> stateMachineRuntimePersister) {
    return new DefaultStateMachineService<States, Events>(stateMachineFactory, stateMachineRuntimePersister);
}

A logic using a StateMachineService in this sample is show below.

private synchronized StateMachine<States, Events> getStateMachine(String machineId) throws Exception {
    listener.resetMessages();
    if (currentStateMachine == null) {
        currentStateMachine = stateMachineService.acquireStateMachine(machineId);
        currentStateMachine.addStateListener(listener);
        currentStateMachine.start();
    } else if (!ObjectUtils.nullSafeEquals(currentStateMachine.getId(), machineId)) {
        stateMachineService.releaseStateMachine(currentStateMachine.getId());
        currentStateMachine.stop();
        currentStateMachine = stateMachineService.acquireStateMachine(machineId);
        currentStateMachine.addStateListener(listener);
        currentStateMachine.start();
    }
    return currentStateMachine;
}

Let’s get into actual demo. Run the boot based sample application:

# java -jar spring-statemachine-samples-datapersist-1.2.9.RELEASE.jar
[Note]Note

Profile jpa is enabled on default in application.yml. If you want to try other backends, enable mongo or redis profile.

# java -jar spring-statemachine-samples-datapersist-1.2.9.RELEASE.jar --spring.profiles.active=jpa
# java -jar spring-statemachine-samples-datapersist-1.2.9.RELEASE.jar --spring.profiles.active=mongo
# java -jar spring-statemachine-samples-datapersist-1.2.9.RELEASE.jar --spring.profiles.active=redis

Accessing application via http://localhost:8080 brings up a new constructed machine with every request and you can choose to send events to a machine. Possible events and machine configuration are updated from a database with every request.

Machines in this sample have a simple configuration with states 'S1' to 'S6' and events 'E1' to 'E6' transitioning machine between those states. Two machine identifiers 'datajpapersist1' and 'datajpapersist2' can be used to request particular machine.

sm datajpapersist 1

Sample defaults to using machine 'datajpapersist1' and goes to its initial state 'S1'.

sm datajpapersist 2

If events 'E1' and 'E2' are sent into machine 'datajpapersist1' its state is persisted as 'S3'.

sm datajpapersist 3

If requesting machine 'datajpapersist1' by not sending any events, machine is restored back to its persisted state 'S3'.