Distributed State Machines using Java Spring State Machine Framework

Niyazi Ozdinc Celikel
4 min readJan 5, 2020

--

The aim of this story is to provide a skeleton for constructing distributed statemachines via integrating Apache Zookeeper along with Java Spring State Machine framework.

Before going implementation and integration details regarding Apache Zookeeper, here is a brief introduction for Java Spring State Machine framework, reference is from current documentation: The idea is that your application is now in and may exist in a finite number of states. Then something happens that takes your application from one state to the next. A state machine is driven by triggers, which are based on either events or timers.

Distributed property for state machines comes with an abstraction layer on top of default state machine implementation, for the time being, only one implementation exists which is based on Apache Zookeeper, a coordination service for distributed applications. My suggestion is reading the offical Spring State Machine documentation via this link before going deeper.

For this example, state machine will have 3 states -and obviously, will have 3 state transitions- as visualized below:

States & State Transitions

In order to use spring-state-machine framework, maven will be used for injecting required dependencies into project structure:

<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>

After importing this dependency, do not forget the clean target/ directory, install project files to the local repository again and generate classes under target/ directory via maven commands below:

mvn clean && mvn install && mvn compile

On this example, there will be 4 containers which will be hosts for state machines. These containers will be orchestrated via docker-compose. 3 of the containers will execute an identical and distributed state machine, the remaining one will be host for the Apache Zookeeper binaries. While state machines are being executed in each of the containers, they all will be using the same Apache Zookeeper instance -znode- for being distributed. Hence, distributed property for state machines is established via using Apache Zookeeper. Spring has also great example for understanding this usage.

First of all, a Spring Configuration class is needed, which is a class annotated with @Configuration and plays a role as a container for bean definitions.

@Configuration                       
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<States, Events> {...}

The next to-do is configuring states, events -triggers which starts state transitions- and transition actions -triggered during action processing, you can read this chapter- inside configuration class in order to be more concrete about “States & State Transitions” picture above:

@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
throws Exception {
transitions
.withExternal()
.source(States.UNPAID).
target(States.WAITING_FOR_RECEIVE)
.event(Events.PAY)
.action(transitionAction())

.and()

.withExternal()
.source(States.WAITING_FOR_RECEIVE)
.target(States.DONE)
.event(Events.RECEIVE)
.action(transitionAction())

.and()

.withExternal()
.source(States.DONE).target(States.UNPAID)
.event(Events.STARTFROMSCRATCH)
.action(transitionAction());
}

Inside Configuration class, there is need to instantiate an ensemble, by passing curotorClient which is an API for simplifying usage of Zookeeper functionalities and basePath which indicates the base zookeeper path -which will be used for creating znodes, highly suggested to read official documentation in order to get used to terminology-:

@Bean                           
public StateMachineEnsemble<States, Events> stateMachineEnsemble() throws Exception {
return new ZookeeperStateMachineEnsemble<States, Events>(curatorClient(), "/zkPath"); }

stateMachineEnsemble bean will soon be used for declaring distributed state machine in the last step.

While creating an CuratorFramework instance, IP/hostname of the machine which hosts Apache Zookeeper binaries should be known. As we stated above, docker-compose is used as orchestration tool, so that hostname of the machine which locates Apache Zookeeper is passed as environment variable. In addition to that, default port for zookeeper -which is 2181- will be used. Using hostname and port information, connection string can be constructed for further purposes.

@Bean                           
public CuratorFramework curatorClient() throws Exception { String zkConnectionString = "zookeeper:2181"; RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); CuratorFramework client = CuratorFrameworkFactory.builder() .defaultData(new byte[0]) .retryPolicy(retryPolicy) .connectString(zkConnectionString) .build();
client.start();
CuratorFrameworkState state = client.getState(); System.out.println("State ----> " + state.name());
return client;
}

And last step, declaring distributed state machine by using stateMachineEnsemble bean which is injected as above:

@Override                           
public void configure(StateMachineConfigurationConfigurer
<States, Events> config) throws Exception {
config
.withDistributed()
.ensemble(stateMachineEnsemble());}

When the zookeeper up and the statemachines are connected, znode can be queried either with built-in Zookeeper commands or CuratorFramework built-in methods:

Using “ls” commands for querying
Using CuratorFramework built-in methods for querying

You can see the details of implementation from this link & this one.

Happy new year & happy coding!

--

--

Responses (1)