Statemachine (Spring Boot)

Introduction

Spring Statemachine (SSM) is a framework for application developers to use state machine concepts with Spring applications.


State machines are powerful because behavior is always guaranteed to be consistent, making it relatively easy to debug. This is because operational rules are written in stone when the machine is started. The idea is that your application may exist in a finite number of states and certain predefined triggers can take your application from one state to the next. Such triggers can be based on either events or timers.

It is much easier to define high level logic outside of your application and then rely on the state machine to manage state. You can interact with the state machine by sending an event, listening for changes or simply request a current state.


References

  • Spring Statemachine (project)

https://projects.spring.io/spring-statemachine/

  • A Guide to the Spring State Machine Project (baeldung. June 9, 2017)

https://www.baeldung.com/spring-state-machine

  • Introducing Spring State Machine

https://nagilla748.medium.com/introducing-spring-state-machine-77d446f85e13

  • Ejemplo de máquina de estados con Spring Statemachine

https://picodotdev.github.io/blog-bitix/2019/03/ejemplo-de-maquina-de-estados-con-spring-statemachine/


Concepts (basic)

Triggers


Transitions

Usually transitions are defined with method withExternal() but in the cases of the statuses of type choice, fork and join are defined with methods withChoice(), withFork() and withJoin().

  • withExternal()

  • withChoice()

The methods used are first(), then() and last() that a guard clause in the first two determine to which state it is transitioned.

  • withFork()

The fork causes the state machine to execute two paths in parallel, when the correct events are sent, the final states of the two branches of the substate are reached and the corresponding join state is automatically passed.

  • withJoin()


Guards

Guards allow to choose transition among several ones in the 'choices'.

A guard clause is an instance of the class Guard that contains a method that returns a boolean according to its logic. If it is true the associated state is selected.

Eg:

public class RandomGuard implements Guard<Main.States, Main.Events> {


@Override

public boolean evaluate(StateContext<Main.States, Main.Events> context) {

return new Random().nextBoolean();

}

}


Actions

Event listeners (mechanism)

The opposite to the listener concept are the interceptors.

Interceptors (mechanism)

If one need to do some application activities outside the SM which can lead to break the transition e.g. track changes in a repository, the event listeners mechanism will not be the right choice in case exceptions can be raised. Instead one should use the second option which is the SM interceptor mechanism. The concept of an interceptor is a relatively deep internal feature and thus is not exposed directly through the StateMachine interface. It sticks on an appropriate instance of a SM and acts as stable part of it. The interceptors can break the state change due exception mechanism. An interceptor must be registered via StateMachineAccessor even before the SM has been initialized.


Concepts (advanced)

Distributed SSM doubt

With the Zookeeper SM extension a distributed SM can be gained (spring-statemachine-zookeeper).

Doubt: Is it necessary to setup an Apache ZooKeeper server?


Annotations

The SSM configuration (@Configuration) can have either one of:

@EnableStateMachine - Instance of a SM can be built and started immediately on application startup

@EnableStateMachineFactory - Instance of SM is not created immediately on startup but started through the factory (for scenarios where it is useful to start an instance of SM depending on a business logic)


SM instances

By default, the Spring dependencies container uses the @Scope singleton for the beans so that there is only a single instance, as the machines have state they cannot be shared, you have to create a new one in case you want to use two simultaneously as would be the case of a web application or a process that listens for messages from a queue, using the prototype scope an instance is created each time it is needed. The creation of more state machines is indicated in the documentation that it is somewhat expensive; for avoiding to create them, having several instances and limiting to a certain number a pool of state machines can be used. The documentation example uses Redis for persisting the state machines [https://docs.spring.io/spring-statemachine/docs/2.1.1.RELEASE/reference/htmlsingle/#statemachine-examples-eventservice].


State change events and actions sequence

In a state change, the following sequence of events and actions occurs.

  • The start of the transition is notified.

  • The action associated with the transition is executed.

  • The transition is notified, the exit from the previous state and the entry into the new one.

  • The state and state entry action is executed.

  • The status change is notified.

  • The completion of the transition is notified.


Initiating the SM in a given state for continuing the flow

For continuing the flow of an entity that previously remained in that state, use the method resetStateMachine().


Handling states of different items (applications, orders, etc.)

For handling states of every item coming to system with the same instance of the SM, one need to put the information about the concrete order on the event. For this purpose, an interface can be created, eg for orders:

public interface OrderStateChangeListener {

void onStateChange(State<States, Events> state, Message<Events> message);

}

The order related to the event is wrapped to the appropriate message object context. The publisher and consumers must then be aware of this type of object. While the message is a standard Spring messaging framework object where the payload is of type Events, an order ID is set to the header of this message. The main part of the interception logic is built upon a component named OrdersStateHandler. Creating a lifecycle object handler enables us to instruct the SM accessor to use an additional interceptor for events related handling even before SM instance started:

@Component

public class OrdersStateHandler extends LifecycleObjectSupport {


@Autowired

private StateMachine<States, Events> stateMachine;


private Set<OrderStateChangeListener> listeners = new HashSet<>();


@Override

protected void onInit() throws Exception {

stateMachine

.getStateMachineAccessor()

.doWithAllRegions(new StateMachineFunction<State-MachineAccess<States, Events>>() {

@Override

public void apply(StateMachineAccess<States, Events> function) {

function.addStateMachineInterceptor(new StateMachineInterceptorAdapter<States, Events>() {

@Override

public void preStateChange(State<States, Events> state, Message message) {

listeners.forEach(listener -> listener.onStateChange(state, message));

}

});

}

});

}


public void registerListener(OrderStateChangeListener listener) {

listeners.add(listener);

}


public void handleEvent(Message event, States sourceState)

{

stateMachine.stop();

stateMachine

.getStateMachineAccessor()

.doWithAllRegions(access -> access.resetStateMa-chine(new DefaultStateMachineContext<States, Events>(sourceState, null, null, null)));

stateMachine.start();

stateMachine.sendEvent(event);

}

}

The events now shall not be send directly to the SM, but posted via a handler. One instance of the SM is shared across the application for many different orders. Every time a state change event is published, the handler first does a reset of the SM to the appropriate source state before changing it. At this point the core SM logic has been implemented. Now one need to let publishers and all consumers know about the handler. For the publisher responsible for delivering the order to the customer it looks like this:

stateHandler.handleEvent(

MessageBuilder

.withPayload(Events.deliver)

.setHeader("order-id", orderId)

.build(), States.ASSEMBLED);

Consumer in this case must implement the appropriate interface OrderStateChangeListener and register itself by a handler. A possible consumer listener could be a persistence service updating the state to the database:

@Override

public void onStateChange(State<States, Events> state, Message message) {

Long orderId = message.getHeaders().get("order-id", Long.class);

Order order = repository.findOne(orderId);

order.setState(state.getId());

repository.save(order);

}


Handling different state machine with the same application (subjects, applications, etc.)

Doubt: Is this possible and, if so, how?

With the method withStates() one can define sub-machines or a state hierarchy (eg: s1->s2->s3).