2.1.1.RELEASE
Copyright © 2015, 2016, 2017, 2018, 2019 Pivotal Software, Inc.
Table of Contents
The concept of a state machine is most likely older that any reader of this reference documentation and definitely older than the Java language itself. Description of finite automata dates back to 1943 when gentlemen Warren McCulloch and Walter Pitts wrote a paper about it. Later George H. Mealy presented a state machine concept in 1955 which is known as a Mealy Machine. A year later in 1956 Edward F. Moore presented another paper which is known as a Moore Machine. If you’ve ever read anything about state machines, the names Mealy and Moore should have popped up at some point.
This reference documentation contains the following parts.
Part I, “Introduction” introduction to this reference documentation
Part IV, “Using Spring Statemachine” describes the usage of Spring Statemachine(SSM)
Part VI, “State Machine Examples” more detailed state machine examples
Part VII, “FAQ” frequently asked questions
Part VIII, “Appendices” generic info about used material and state machines
Spring Statemachine(SSM) is a framework for application developers to use traditional state machine concepts with Spring applications. SSM aims to provide the following features:
Before you continue it’s worth to go through appendices Section B.2, “Glossary” and Section B.3, “A State Machines Crash Course” to get a generic idea of what state machines are, mostly because the rest of the documentation expects the reader to be fairly familiar with state machine concepts.
State machines are powerful because behaviour is always guaranteed to be consistent and relatively easily debugged due to how operational rules are written in stone when machine is started. The idea is that your application is and may exist in a finite number of states and then something happens which takes your application from one state to the next. What will drive a state machine are triggers, which are either based on events or timers.
It is much easier to design high level logic outside of your application and then interact with a state machine in various different ways. You can simply interact with a state machine by sending events, listening to what a state machine does or simply requesting the current state.
Traditionally state machines are added to an existing project when developers realize that the code base is starting to look like a plate full of spaghetti. Spaghetti code looks like a never ending, hierarchical structure of IFs, ELSEs and BREAK clauses and probably compilers should ask developers to go home when things are starting to look too complex.
A project is a good candidate to use a state machine if:
You are already trying to implement a state machine if:
If you’re just getting started with Spring Statemachine, this is the section for you! Here we answer the basic “what?”, “how?” and “why?” questions. You’ll find a gentle introduction to Spring Statemachine. We’ll then build our first Spring Statemachine application, discussing some core principles as we go.
Spring Statemachine 2.1.1.RELEASE is built and tested with JDK 8(all artifacts have JDK 7 compatibility) and Spring Framework 5.1.5.RELEASE and doesn’t require any other dependencies outside of Spring Framework within its core system.
Other optional parts like Chapter 31, Using Distributed States has dependencies to
a Zookeeper
, while Part VI, “State Machine Examples” has dependencies
to spring-shell and spring-boot which pulls other dependencies
beyond framework itself. Also optional security and data access has
dependencies to Spring Security and Spring Data Modules.
The following modules are available for Spring Statemachine.
Module | Description |
---|---|
spring-statemachine-core | Core system of a Spring Statemachine. |
spring-statemachine-recipes-common | Common recipes which doesn’t require dependencies outside of a core framework. |
spring-statemachine-kryo |
|
spring-statemachine-data-common | Common support module for |
spring-statemachine-data-jpa | Support module for |
spring-statemachine-data-redis | Support module for |
spring-statemachine-data-mongodb | Support module for |
spring-statemachine-zookeeper |
|
spring-statemachine-test | Support module for state machine testing. |
spring-statemachine-cluster | Support module for Spring Cloud Cluster. |
spring-statemachine-uml | Support module for UI uml modeling with Eclipse Papyrus. |
spring-statemachine-autoconfigure | Support module for |
spring-statemachine-bom | Bill of Materials pom. |
spring-statemachine-starter |
|
Here is a typical build.gradle
file created by https://start.spring.io:
buildscript { ext { springBootVersion = '2.1.3.RELEASE' } repositories { mavenCentral() maven { url "https://repo.spring.io/snapshot" } maven { url "https://repo.spring.io/milestone" } } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") } } apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' group = 'com.example' version = '0.0.1-SNAPSHOT' sourceCompatibility = 1.8 repositories { mavenCentral() maven { url "https://repo.spring.io/snapshot" } maven { url "https://repo.spring.io/milestone" } } ext { springStatemachineVersion = '2.1.1.RELEASE' } dependencies { compile('org.springframework.statemachine:spring-statemachine-starter') testCompile('org.springframework.boot:spring-boot-starter-test') } dependencyManagement { imports { mavenBom "org.springframework.statemachine:spring-statemachine-bom:${springStatemachineVersion}" } }
Note | |
---|---|
Replace |
Having a normal project structure you’d build this with command:
# ./gradlew clean build
Expected Spring Boot packaged fat-jar would be build/libs/demo-0.0.1-SNAPSHOT.jar
.
Note | |
---|---|
You don’t need repos |
Here is a typical pom.xml
file created by https://start.spring.io:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>gs-statemachine</name> <description>Demo project for Spring Statemachine</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <spring-statemachine.version>2.1.1.RELEASE</spring-statemachine.version> </properties> <dependencies> <dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-bom</artifactId> <version>${spring-statemachine.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <repositories> <repository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> <snapshots> <enabled>true</enabled> </snapshots> </repository> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> <snapshots> <enabled>true</enabled> </snapshots> </pluginRepository> <pluginRepository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </pluginRepository> </pluginRepositories> </project>
Note | |
---|---|
Replace |
Having a normal project structure you’d build this with command:
# mvn clean package
Expected Spring Boot packaged fat-jar would be target/demo-0.0.1-SNAPSHOT.jar
.
Note | |
---|---|
You don’t need repos |
Let’s start by creating a simple Spring Boot Application
class
implementing CommandLineRunner
.
@SpringBootApplication public class Application implements CommandLineRunner { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
Add states and events:
public enum States { SI, S1, S2 } public enum Events { E1, E2 }
Add state machine configuration:
@Configuration @EnableStateMachine public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineConfigurationConfigurer<States, Events> config) throws Exception { config .withConfiguration() .autoStartup(true) .listener(listener()); } @Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.SI) .states(EnumSet.allOf(States.class)); } @Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.SI).target(States.S1).event(Events.E1) .and() .withExternal() .source(States.S1).target(States.S2).event(Events.E2); } @Bean public StateMachineListener<States, Events> listener() { return new StateMachineListenerAdapter<States, Events>() { @Override public void stateChanged(State<States, Events> from, State<States, Events> to) { System.out.println("State change to " + to.getId()); } }; } }
Implement CommandLineRunner
, autowire StateMachine
:
@Autowired private StateMachine<States, Events> stateMachine; @Override public void run(String... args) throws Exception { stateMachine.sendEvent(Events.E1); stateMachine.sendEvent(Events.E2); }
Depending whether you build your application using Gradle
or Maven
it’s run java -jar build/libs/gs-statemachine-0.1.0.jar
or
java -jar target/gs-statemachine-0.1.0.jar
respectively.
What is expected for running this command is a normal Spring Boot output but if you look closely you see lines:
State change to SI State change to S1 State change to S2
Spring Statemachine 1.1 is focusing on security and a better interoperability with web applications.
StateContext
is now a first class citizen with how user can
interact with a State Machine, Chapter 19, Using StateContext.
Spring Statemachine 1.2 is focusing generic enhancements, better UML support and integrations with external config repositories.
Spring Statemachine 1.2.8 contains a bit more functionality normally not seen in a point release but these changes didn’t merit a fork of Spring Statemachine 1.3.
Spring Statemachine 2.0 is focusing on Spring Boot 2.x support.
spring-statemachine-boot
has been renamed to spring-statemachine-autoconfigure
This part of the reference documentation explains the core functionality that Spring Statemachine provides to any Spring based application.
Chapter 11, Statemachine Configuration the generic configuration support.
Chapter 12, State Machine ID the use of machine id.
Chapter 13, State Machine Factories the generic state machine factory support.
Chapter 14, Using Deferred Events the deferred event support.
Chapter 15, Using Scopes the scope support.
Chapter 16, Using Actions the actions support.
Chapter 17, Using Guards the guard support.
Chapter 18, Using Extended State the extended state support.
Chapter 19, Using StateContext the state context support.
Chapter 20, Triggering Transitions the use of triggers.
Chapter 21, Listening State Machine Events the use of state machine listeners.
Chapter 22, Context Integration the generic Spring application context support.
Chapter 23, State Machine Accessor the state machine internal accessor support.
Chapter 24, State Machine Interceptor the state machine error handling support.
Chapter 25, State Machine Security the state machine security support.
Chapter 26, State Machine Error Handling the state machine interceptor support.
Chapter 27, State Machine Services the state machine service support.
Chapter 28, Persisting State Machine the state machine persisting support.
Chapter 29, Spring Boot Support the Spring Boot support.
Chapter 30, Monitoring State Machine the monitoring and trancing support.
Chapter 31, Using Distributed States the distributed state machine support.
Chapter 32, Testing Support the state machine testing support.
Chapter 33, Eclipse Modeling Support the state machine uml modeling support.
Chapter 34, Repository Support the state machine repository config support.
One of the common tasks when using a Statemachine is to design its runtime configuration. This chapter will focus on how Spring Statemachine is configured and how it leverages Spring’s lightweight IoC containers to simplify the application internals to make it more manageable.
Note | |
---|---|
Configuration examples in this section are not feature complete, i.e. you always need to have definitions of both states and transitions, otherwise state machine configuration would be ill-formed. We have simply made code snippets less verbose by leaving other needed parts away. |
We use familiar spring enabler annotations to ease configuration. Two annotations exists, @EnableStateMachine and @EnableStateMachineFactory. These annotations if placed in a @Configuration class will enable some basic functionality needed by a state machines.
@EnableStateMachine is used when a configuration wants to create an
instance of a StateMachine. Usually @Configuration class extends adapters
EnumStateMachineConfigurerAdapter
or StateMachineConfigurerAdapter
which
allows user to override configuration callback methods. We automatically
detect if user is using these adapter classes and modify runtime configuration
logic.
@EnableStateMachineFactory is used when a configuration wants to create an instance of a StateMachineFactory.
Note | |
---|---|
Usage examples of these are shown in below sections. |
We’ll get into more complex configuration examples a bit later but
let’s first start with a something simple. For most simple state
machine you just use EnumStateMachineConfigurerAdapter
and define
possible states, choose initial and optional end state.
@Configuration @EnableStateMachine public class Config1Enums extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.S1) .end(States.SF) .states(EnumSet.allOf(States.class)); } }
It’s also possible to use strings instead of enums as states and
events by using StateMachineConfigurerAdapter
as shown below. Most
of a configuration examples is using enums but generally speaking
strings and enums can be just interchanged.
@Configuration @EnableStateMachine public class Config1Strings extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineStateConfigurer<String, String> states) throws Exception { states .withStates() .initial("S1") .end("SF") .states(new HashSet<String>(Arrays.asList("S1","S2","S3","S4"))); } }
Note | |
---|---|
Using enums will bring more safe set of states and event types but limits possible combinations to compile time. Strings don’t have this limitation and allows user to use more dynamic ways to build state machine configurations but doesn’t allow same level of safety. |
Hierarchical states can be defined by using multiple withStates()
calls where parent()
can be used to indicate that these
particular states are sub-states of some other state.
@Configuration @EnableStateMachine public class Config2 extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.S1) .state(States.S1) .and() .withStates() .parent(States.S1) .initial(States.S2) .state(States.S2); } }
There are no special configuration methods to mark a collection of states to be part of an orthogonal state. To put it simple, orthogonal state is created when same hierarchical state machine has multiple set of states each having a initial state. Because an individual state machine can only have one initial state, multiple initial states must mean that a specific state must have multiple independent regions.
@Configuration @EnableStateMachine public class Config10 extends EnumStateMachineConfigurerAdapter<States2, Events> { @Override public void configure(StateMachineStateConfigurer<States2, Events> states) throws Exception { states .withStates() .initial(States2.S1) .state(States2.S2) .and() .withStates() .parent(States2.S2) .initial(States2.S2I) .state(States2.S21) .end(States2.S2F) .and() .withStates() .parent(States2.S2) .initial(States2.S3I) .state(States2.S31) .end(States2.S3F); } }
When working with persisting machines with regions or generally
relying any functionalities to reset a machine it may be required
to have a dedicated id for a region itself. On default this id
is just a generated UUID. As shown below StateConfigurer
has
a method region(String id)
for it.
@Configuration @EnableStateMachine public class Config10RegionId extends EnumStateMachineConfigurerAdapter<States2, Events> { @Override public void configure(StateMachineStateConfigurer<States2, Events> states) throws Exception { states .withStates() .initial(States2.S1) .state(States2.S2) .and() .withStates() .parent(States2.S2) .region("R1") .initial(States2.S2I) .state(States2.S21) .end(States2.S2F) .and() .withStates() .parent(States2.S2) .region("R2") .initial(States2.S3I) .state(States2.S31) .end(States2.S3F); } }
We support three different types of transitions, external
,
internal
and local
. Transitions are either triggered by a signal
which is an event sent into a state machine or a timer.
@Configuration @EnableStateMachine public class Config3 extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.S1) .states(EnumSet.allOf(States.class)); } @Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.S1).target(States.S2) .event(Events.E1) .and() .withInternal() .source(States.S2) .event(Events.E2) .and() .withLocal() .source(States.S2).target(States.S3) .event(Events.E3); } }
Guards are used to protect state transitions. Interface Guard is used to do an evaluation where method has access to a StateContext.
@Configuration @EnableStateMachine public class Config4 extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.S1).target(States.S2) .event(Events.E1) .guard(guard()) .and() .withExternal() .source(States.S2).target(States.S3) .event(Events.E2) .guardExpression("true"); } @Bean public Guard<States, Events> guard() { return new Guard<States, Events>() { @Override public boolean evaluate(StateContext<States, Events> context) { return true; } }; } }
In above two different types of guard configurations are used. Firstly a
simple Guard is created as a bean and attached to transition between
states S1
and S2
.
Secondly a simple SPeL expression can be used as a guard where
expression must return a BOOLEAN
value. Behind a scenes this
expression based guard is a SpelExpressionGuard. This was attached to
transition between states S2
and S3
. Both guard in above sample
always evaluate to true.
Actions can be defined to be executed with transitions and states itself. Action is always executed as a result of a transition which originates from a trigger.
@Configuration @EnableStateMachine public class Config51 extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.S1) .target(States.S2) .event(Events.E1) .action(action()); } @Bean public Action<States, Events> action() { return new Action<States, Events>() { @Override public void execute(StateContext<States, Events> context) { // do something } }; } }
In above a single Action
is defined as bean action
and associated
with a transition from S1
to S2
.
@Configuration @EnableStateMachine public class Config52 extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.S1, action()) .state(States.S1, action(), null) .state(States.S2, null, action()) .state(States.S2, action()) .state(States.S3, action(), action()); } @Bean public Action<States, Events> action() { return new Action<States, Events>() { @Override public void execute(StateContext<States, Events> context) { // do something } }; } }
Note | |
---|---|
Usually you would not define same |
In above a single Action
is defined as bean action
and associated
with states S1
, S2
and S3
. There is more going on there which
needs more clarification:
S1
.
S1
and left exit action empty.
S2
and left entry action empty.
S2
.
S3
.
S1
is used twice with initial()
and state()
functions. This is only needed if you want to define entry or exit
actions with initial state.
Important | |
---|---|
Defining action with |
State actions are executed differently compared to entry and exit actions simply because execution happens after state has been entered and can be cancelled if state exit happens before particular action has been completed.
State Actions are executed using a normal Spring TaskScheduler
wrapped within a Runnable
which may get cancelled via
ScheduledFuture
. What this means is that whatever you’re doing in your
action, you need to be able to catch InterruptedException
or generally
periodically check if Thread is interrupted.
Below shows typical config which uses default IMMEDIATE_CANCEL which would simply cancel running task immediately when state is complete.
@Configuration @EnableStateMachine static class Config1 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception { config .withConfiguration() .stateDoActionPolicy(StateDoActionPolicy.IMMEDIATE_CANCEL); } @Override public void configure(StateMachineStateConfigurer<String, String> states) throws Exception { states .withStates() .initial("S1") .state("S2", context -> {}) .state("S3"); } @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions .withExternal() .source("S1") .target("S2") .event("E1") .and() .withExternal() .source("S2") .target("S3") .event("E2"); } }
Policy can be set to TIMEOUT_CANCEL together with a global timeout per machine. This changes state behaviour to wait action completion before cancel is requested.
@Override public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception { config .withConfiguration() .stateDoActionPolicy(StateDoActionPolicy.TIMEOUT_CANCEL) .stateDoActionPolicyTimeout(10, TimeUnit.SECONDS); }
If Event directly take machine into a state so that event headers are available to particular action, it is also possible to use dedicated event header to instruct a specific timeout which is defined in millis. Reserved header value StateMachineMessageHeaders.HEADER_DO_ACTION_TIMEOUT is used for this.
@Autowired StateMachine<String, String> stateMachine; void sendEventUsingTimeout() { stateMachine.sendEvent(MessageBuilder .withPayload("E1") .setHeader(StateMachineMessageHeaders.HEADER_DO_ACTION_TIMEOUT, 5000) .build()); }
User can always catch exceptions manually but with actions defined for
transitions it is possible to define error action which is called if
exception is raised. Exception is then available from a StateContext
passed to that action.
@Configuration @EnableStateMachine public class Config53 extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.S1) .target(States.S2) .event(Events.E1) .action(action(), errorAction()); } @Bean public Action<States, Events> action() { return new Action<States, Events>() { @Override public void execute(StateContext<States, Events> context) { throw new RuntimeException("MyError"); } }; } @Bean public Action<States, Events> errorAction() { return new Action<States, Events>() { @Override public void execute(StateContext<States, Events> context) { // RuntimeException("MyError") added to context Exception exception = context.getException(); exception.getMessage(); } }; } }
Similar logic can be done manually for every action if needed.
@Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.S1) .target(States.S2) .event(Events.E1) .action(Actions.errorCallingAction(action(), errorAction())); }
Similar logic for error handling what is available for transition actions is also available for actions defined for state behaviour and its entry and exit.
For these StateConfigurer
has methods stateEntry
, stateDo
and
stateExit
to define error
action together with an actual action
.
@Configuration @EnableStateMachine public class Config55 extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.S1) .stateEntry(States.S2, action(), errorAction()) .stateDo(States.S2, action(), errorAction()) .stateExit(States.S2, action(), errorAction()) .state(States.S3); } @Bean public Action<States, Events> action() { return new Action<States, Events>() { @Override public void execute(StateContext<States, Events> context) { throw new RuntimeException("MyError"); } }; } @Bean public Action<States, Events> errorAction() { return new Action<States, Events>() { @Override public void execute(StateContext<States, Events> context) { // RuntimeException("MyError") added to context Exception exception = context.getException(); exception.getMessage(); } }; } }
Pseudo state configuration is usually done by configuring states and transitions. Pseudo states are automatically added to state machine as states.
Simply mark a particular state as initial state by using initial()
method. There are two methods where one takes extra argument to define
an initial action. This initial action is good for example initialize
extended state variables.
@Configuration @EnableStateMachine public class Config11 extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.S1, initialAction()) .end(States.SF) .states(EnumSet.allOf(States.class)); } @Bean public Action<States, Events> initialAction() { return new Action<States, Events>() { @Override public void execute(StateContext<States, Events> context) { // do something initially } }; } }
Simply mark a particular state as end state by using end()
method.
This can be done max one time per individual sub-machine or region.
@Configuration @EnableStateMachine public class Config1Enums extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.S1) .end(States.SF) .states(EnumSet.allOf(States.class)); } }
History state can be defined once for each individual state machine.
You need to choose its state identifier and History.SHALLOW
or
History.DEEP
respectively.
@Configuration @EnableStateMachine public class Config12 extends EnumStateMachineConfigurerAdapter<States3, Events> { @Override public void configure(StateMachineStateConfigurer<States3, Events> states) throws Exception { states .withStates() .initial(States3.S1) .state(States3.S2) .and() .withStates() .parent(States3.S2) .initial(States3.S2I) .state(States3.S21) .state(States3.S22) .history(States3.SH, History.SHALLOW); } @Override public void configure(StateMachineTransitionConfigurer<States3, Events> transitions) throws Exception { transitions .withHistory() .source(States3.SH) .target(States3.S22); } }
Also as shown above, optionally it is possible to define a default transition from a history state into a state vertex in a same machine. This transition takes place as a default if for example machine has never been entered, thus no history would be available. If default state transition is not defined, then normal entry into a region is done. This default transition is also used if machine’s history is a final state.
Choice needs to be defined in both states and transitions to work
properly. Mark particular state as choice state by using choice()
method. This state needs to match source state when transition is
configured for this choice.
Transition is configured using withChoice()
where you define source
state and first/then/last
structure which is equivalent to normal
if/elseif/else
. With first
and then
you can specify a guard just
like you’d use a condition with if/elseif
clauses.
Transition needs to be able to exist so make sure last
is used.
Otherwise configuration is ill-formed.
@Configuration @EnableStateMachine public class Config13 extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.SI) .choice(States.S1) .end(States.SF) .states(EnumSet.allOf(States.class)); } @Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withChoice() .source(States.S1) .first(States.S2, s2Guard()) .then(States.S3, s3Guard()) .last(States.S4); } @Bean public Guard<States, Events> s2Guard() { return new Guard<States, Events>() { @Override public boolean evaluate(StateContext<States, Events> context) { return false; } }; } @Bean public Guard<States, Events> s3Guard() { return new Guard<States, Events>() { @Override public boolean evaluate(StateContext<States, Events> context) { return true; } }; } }
Actions can be executed with both incoming and outgoing transitions of a choice pseudostate. As seeing from below example, one dummy lambda action is defined leading into a choice state and one similar dummy lambda action defined for one outgoing transition where it also define an error action.
@Configuration @EnableStateMachine public class Config23 extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.SI) .choice(States.S1) .end(States.SF) .states(EnumSet.allOf(States.class)); } @Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.SI) .action(c -> { // action with SI-S1 }) .target(States.S1) .and() .withChoice() .source(States.S1) .first(States.S2, c -> { return true; }) .last(States.S3, c -> { // action with S1-S3 }, c -> { // error callback for action S1-S3 }); } }
Note | |
---|---|
Junction have same api format meaning actions can be defined similarly. |
Junction needs to be defined in both states and transitions to work
properly. Mark particular state as choice state by using junction()
method. This state needs to match source state when transition is
configured for this choice.
Transition is configured using withJunction()
where you define source
state and first/then/last
structure which is equivalent to normal
if/elseif/else
. With first
and then
you can specify a guard just
like you’d use a condition with if/elseif
clauses.
Transition needs to be able to exist so make sure last
is used.
Otherwise configuration is ill-formed.
@Configuration @EnableStateMachine public class Config20 extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.SI) .junction(States.S1) .end(States.SF) .states(EnumSet.allOf(States.class)); } @Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withJunction() .source(States.S1) .first(States.S2, s2Guard()) .then(States.S3, s3Guard()) .last(States.S4); } @Bean public Guard<States, Events> s2Guard() { return new Guard<States, Events>() { @Override public boolean evaluate(StateContext<States, Events> context) { return false; } }; } @Bean public Guard<States, Events> s3Guard() { return new Guard<States, Events>() { @Override public boolean evaluate(StateContext<States, Events> context) { return true; } }; } }
Note | |
---|---|
Difference between choice and junction is purely academic as both are
implemented with |
Fork needs to be defined in both states and transitions to work
properly. Mark particular state as choice state by using fork()
method. This state needs to match source state when transition is
configured for this fork.
Target state needs to be a super state or immediate states in regions. Using a super state as target will take all regions into initial states. Targeting individual state give more controlled entry into regions.
@Configuration @EnableStateMachine public class Config14 extends EnumStateMachineConfigurerAdapter<States2, Events> { @Override public void configure(StateMachineStateConfigurer<States2, Events> states) throws Exception { states .withStates() .initial(States2.S1) .fork(States2.S2) .state(States2.S3) .and() .withStates() .parent(States2.S3) .initial(States2.S2I) .state(States2.S21) .state(States2.S22) .end(States2.S2F) .and() .withStates() .parent(States2.S3) .initial(States2.S3I) .state(States2.S31) .state(States2.S32) .end(States2.S3F); } @Override public void configure(StateMachineTransitionConfigurer<States2, Events> transitions) throws Exception { transitions .withFork() .source(States2.S2) .target(States2.S22) .target(States2.S32); } }
Join needs to be defined in both states and transitions to work
properly. Mark particular state as choice state by using join()
method. This state doesn’t need to match either source states or
target state in a transition configuration.
Select a target state where transition goes when all source states has been joined. If you use state hosting regions as source, end states of a regions are used as joins. Otherwise you can pick any states from a regions.
@Configuration @EnableStateMachine public class Config15 extends EnumStateMachineConfigurerAdapter<States2, Events> { @Override public void configure(StateMachineStateConfigurer<States2, Events> states) throws Exception { states .withStates() .initial(States2.S1) .state(States2.S3) .join(States2.S4) .state(States2.S5) .and() .withStates() .parent(States2.S3) .initial(States2.S2I) .state(States2.S21) .state(States2.S22) .end(States2.S2F) .and() .withStates() .parent(States2.S3) .initial(States2.S3I) .state(States2.S31) .state(States2.S32) .end(States2.S3F); } @Override public void configure(StateMachineTransitionConfigurer<States2, Events> transitions) throws Exception { transitions .withJoin() .source(States2.S2F) .source(States2.S3F) .target(States2.S4) .and() .withExternal() .source(States2.S4) .target(States2.S5); } }
It is also possible to have multiple transitions originating from a join state. It this case it is advised to use guards and define those so that only one guard evaluates TRUE at any given time as otherwise transition behaviour is not predicted. This is shown above where guard simply checks if extended state has variables.
@Configuration @EnableStateMachine public class Config22 extends EnumStateMachineConfigurerAdapter<States2, Events> { @Override public void configure(StateMachineStateConfigurer<States2, Events> states) throws Exception { states .withStates() .initial(States2.S1) .state(States2.S3) .join(States2.S4) .state(States2.S5) .end(States2.SF) .and() .withStates() .parent(States2.S3) .initial(States2.S2I) .state(States2.S21) .state(States2.S22) .end(States2.S2F) .and() .withStates() .parent(States2.S3) .initial(States2.S3I) .state(States2.S31) .state(States2.S32) .end(States2.S3F); } @Override public void configure(StateMachineTransitionConfigurer<States2, Events> transitions) throws Exception { transitions .withJoin() .source(States2.S2F) .source(States2.S3F) .target(States2.S4) .and() .withExternal() .source(States2.S4) .target(States2.S5) .guardExpression("!extendedState.variables.isEmpty()") .and() .withExternal() .source(States2.S4) .target(States2.SF) .guardExpression("extendedState.variables.isEmpty()"); } }
Exit and Entry Points can be used to do more controlled exit and entry from and into a submachines.
@Configuration @EnableStateMachine static class Config21 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineStateConfigurer<String, String> states) throws Exception { states .withStates() .initial("S1") .state("S2") .state("S3") .and() .withStates() .parent("S2") .initial("S21") .entry("S2ENTRY") .exit("S2EXIT") .state("S22"); } @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions .withExternal() .source("S1").target("S2") .event("E1") .and() .withExternal() .source("S1").target("S2ENTRY") .event("ENTRY") .and() .withExternal() .source("S22").target("S2EXIT") .event("EXIT") .and() .withEntry() .source("S2ENTRY").target("S22") .and() .withExit() .source("S2EXIT").target("S3"); } }
As shown above you need to mark particular states as exit and entry states. Then you create a normal transitions into those states and also specify withExit() and withEntry() where those states will exit and entry respectively.
Some of a common state machine configuration can be set via a
ConfigurationConfigurer
. This allows to set BeanFactory
,
TaskExecutor
, TaskScheduler
, autostart flag for a state machine
and register StateMachineListener
instances.
@Configuration @EnableStateMachine public class Config17 extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineConfigurationConfigurer<States, Events> config) throws Exception { config .withConfiguration() .autoStartup(true) .machineId("myMachineId") .beanFactory(new StaticListableBeanFactory()) .taskExecutor(new SyncTaskExecutor()) .taskScheduler(new ConcurrentTaskScheduler()) .listener(new StateMachineListenerAdapter<States, Events>()) .transitionConflictPolicy(TransitionConflictPolicy.CHILD); } }
State machine autoStartup
flag is disabled by default because all
instances handling sub-states are controlled by a state machine itself
and cannot be started automatically. Also it is much safer to leave
this decision to a user whether a machine should be started
automatically or not. This flag will only control an autostart of a
top-level state machine.
Setting machineId
within a configuration is simply a convenience if
user wants or needs to do it here.
Setting a BeanFactory
, TaskExecutor
or TaskScheduler
exist for
convenience for a user and are also use within a framework itself.
Registering StateMachineListener
instances is also partly for
convenience but is required if user wants to catch callback during a
state machine lifecycle like getting notified of a state machine
start/stop events. Naturally it is not possible to listen a state
machine start events if autoStartup
is enabled unless listener can
be registered during a configuration phase.
transitionConflictPolicy
can be used in cases where multiple
transition paths could be selected. One usual use case for this is if
machine contains anonymous transitions leading out from a sub-state
and a parent state and user want to define a policy which one will be
selected. This is a global setting within a machine instance and
default to CHILD.
DistributedStateMachine
is configured via withDistributed()
which
allows to set a StateMachineEnsemble
which if exists automatically
wraps created StateMachine
with DistributedStateMachine
and
enables distributed mode.
@Configuration @EnableStateMachine public class Config18 extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineConfigurationConfigurer<States, Events> config) throws Exception { config .withDistributed() .ensemble(stateMachineEnsemble()); } @Bean public StateMachineEnsemble<States, Events> stateMachineEnsemble() throws Exception { // naturally not null but should return ensemble instance return null; } }
More about distributed states, refer to section Chapter 31, Using Distributed States.
StateMachineModelVerifier
is an interface what is used internally to
do some sanity checks for a state machine structure. Its purpose is to
fail fast early instead of letting common configuration errors into a
state machine itself. On default verifier is automatically enabled and
DefaultStateMachineModelVerifier
implementation is used.
With withVerifier()
user can disable verifier or set a custom one if
needed.
@Configuration @EnableStateMachine public class Config19 extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineConfigurationConfigurer<States, Events> config) throws Exception { config .withVerifier() .enabled(true) .verifier(verifier()); } @Bean public StateMachineModelVerifier<States, Events> verifier() { return new StateMachineModelVerifier<States, Events>() { @Override public void verify(StateMachineModel<States, Events> model) { // throw exception indicating malformed model } }; } }
More about config model, refer to section Section 56.1, “StateMachine Config Model”.
Note | |
---|---|
Config methods |
StateMachineModelFactory
is a hook to configure statemachine model
without using a manual configuration. Essentially it is a third party
integration to integrate into a configuration model.
StateMachineModelFactory
can be hooked into a configuration model by
using a StateMachineModelConfigurer
as shown above.
@Configuration @EnableStateMachine public static class Config1 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineModelConfigurer<String, String> model) throws Exception { model .withModel() .factory(modelFactory()); } @Bean public StateMachineModelFactory<String, String> modelFactory() { return new CustomStateMachineModelFactory(); } }
As a custom example CustomStateMachineModelFactory
would simply
define two states, S1 and S2 and an event E1 between those
states.
public static class CustomStateMachineModelFactory implements StateMachineModelFactory<String, String> { @Override public StateMachineModel<String, String> build() { ConfigurationData<String, String> configurationData = new ConfigurationData<>(); Collection<StateData<String, String>> stateData = new ArrayList<>(); stateData.add(new StateData<String, String>("S1", true)); stateData.add(new StateData<String, String>("S2")); StatesData<String, String> statesData = new StatesData<>(stateData); Collection<TransitionData<String, String>> transitionData = new ArrayList<>(); transitionData.add(new TransitionData<String, String>("S1", "S2", "E1")); TransitionsData<String, String> transitionsData = new TransitionsData<>(transitionData); StateMachineModel<String, String> stateMachineModel = new DefaultStateMachineModel<String, String>(configurationData, statesData, transitionsData); return stateMachineModel; } @Override public StateMachineModel<String, String> build(String machineId) { return build(); } }
Note | |
---|---|
Defining a custom model is usually not what end user is looking for, although it is possible, however it is a central concept of allowing external access to this configuration model. |
Example of using this model factory integration can be found from Chapter 33, Eclipse Modeling Support. More generic info about custom model integration can be found from Chapter 56, Developer Documentation.
When defining actions, guards or any other references from a
configuration there are things to remember how Spring Framework works
with beans. In below we have defined a normal configuration with
states S1
and S2
and 4 transitions between those. All transitions
are either guarded by guard1
or guard2
. Pay attention that
guard1
is created as a real bean because it’s annotated with a
@Bean, while guard2
is not.
What this mean is that event E3
would get guard2
condition as
TRUE
and E4
would get guard2
condition as FALSE
as those are
simply coming from a plain method calls to those functions.
However because guard1
is defined as a @Bean, it is proxied by a
Spring Framework, thus additional calls to its method will result
only one instantiation of that instance. Event E1
would get first
proxied instance with condition TRUE
while event E2
would get same
instance with TRUE
condition while method call was defined with
FALSE
. This is not a Spring State Machine specific behaviour, it’s
just how Spring Framework works with Beans.
@Configuration @EnableStateMachine public class Config1 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineStateConfigurer<String, String> states) throws Exception { states .withStates() .initial("S1") .state("S2"); } @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions .withExternal() .source("S1").target("S2").event("E1").guard(guard1(true)) .and() .withExternal() .source("S1").target("S2").event("E2").guard(guard1(false)) .and() .withExternal() .source("S1").target("S2").event("E3").guard(guard2(true)) .and() .withExternal() .source("S1").target("S2").event("E4").guard(guard2(false)); } @Bean public Guard<String, String> guard1(final boolean value) { return new Guard<String, String>() { @Override public boolean evaluate(StateContext<String, String> context) { return value; } }; } public Guard<String, String> guard2(final boolean value) { return new Guard<String, String>() { @Override public boolean evaluate(StateContext<String, String> context) { return value; } }; } }
Various classes and interfaces use machineId
either as a variable or
parameter in a methods. This chapter takes a closer look how
machineId
relates to normal machine operation and instantiation.
During a runtime machineId
really don’t have any big operational
role except to distinguish machines from each other for example when
following logs or doing deeper debugging. Having a lot of different
machine instances quickly gets user lost in translation if there is
no easy way to identify these instances and option to set this
machineId
was given to a user.
Setting machineId
via JavaConfig as mymachine
then exposes that
for logs as shown above. This same machineId
is also available via
method StateMachine.getId()
.
@Override public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception { config .withConfiguration() .machineId("mymachine"); }
11:23:54,509 INFO main support.LifecycleObjectSupport [main] - started S2 S1 / S1 / uuid=8fe53d34-8c85-49fd-a6ba-773da15fcaf1 / id=mymachine
Note | |
---|---|
Manual builder Section 13.2, “State Machine via Builder” uses same config interface meaning behaviour would be equivalent. |
You’ll see same machineId
getting configured if you use a
StateMachineFactory and request a new machine using id.
StateMachineFactory<String, String> factory = context.getBean(StateMachineFactory.class); StateMachine<String, String> machine = factory.getStateMachine("mymachine");
Behind a scenes all machine configurations are first translated into a StateMachineModel so that StateMachineFactory don’t need to know from where configuration originated as machine can be built from JavaConfig, UML or Repository. If user wants to go crazy a custom StateMachineModel can also be used which would be a lowest possible level to define configuration.
What all these has to do with a machineId
?
StateMachineModelFactory also have a method StateMachineModel<S, E>
build(String machineId)
which a StateMachineModelFactory
implementation may choose to use.
RepositoryStateMachineModelFactory Chapter 34, Repository Support uses
machineId
to support different configurations in a persistent
storage used via Spring Data Repository interfaces. For example both
StateRepository and TransitionRepository have a method List<T>
findByMachineId(String machineId)
order to build different states and
transitions by a machineId
. With
RepositoryStateMachineModelFactory if machineId
is used as empty
or NULL defaults to repository config(in a backing persistent model)
without known machine id.
Note | |
---|---|
UmlStateMachineModelFactory currently doesn’t distinguish between different machine id’s as uml source is always coming from a same file. Thought this may get changed in future releases. |
There are use cases when state machine needs to be created dynamically instead of defining static configuration at compile time. For example if there are custom components which are using its own state machines and these components are created dynamically it is impossible to have a static state machined build during the application start. Internally state machines are always build via a factory interfaces and this then gives user an option to use this feature programmatically. Configuration for state machine factory is exactly same as you’ve seen in various examples in this document where state machine configuration is hard coded.
Actually creating a state machine using @EnableStateMachine will work via factory so @EnableStateMachineFactory is merely exposing that factory via its interface.
@Configuration @EnableStateMachineFactory public class Config6 extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.S1) .end(States.SF) .states(EnumSet.allOf(States.class)); } }
Now that you’ve used @EnableStateMachineFactory to create a factory instead of a state machine bean, it can be injected and used as is to request new state machines.
public class Bean3 { @Autowired StateMachineFactory<States, Events> factory; void method() { StateMachine<States,Events> stateMachine = factory.getStateMachine(); stateMachine.start(); } }
Current limitation of factory is that all actions and guard it is associating with created state machine will share a same instances. This means that from your actions and guard you will need to specifically handle a case that same bean will be called by a different state machines. This limitation is something which will be resolved in future releases.
Using adapters shown above has a limitation imposed by its
requirement to work via Spring @Configuration
classes and
application context. While this is a very clear model to configure a
state machine instances it will limit configuration at a compile time
which is not always what a user wants to do. If there is a requirement
to build more dynamic state machines, a simple builder pattern can be
used to construct similar instances. Using strings as states and
events this builder pattern can be used to build fully dynamic state
machines outside of a Spring application context as shown above.
StateMachine<String, String> buildMachine1() throws Exception { Builder<String, String> builder = StateMachineBuilder.builder(); builder.configureStates() .withStates() .initial("S1") .end("SF") .states(new HashSet<String>(Arrays.asList("S1","S2","S3","S4"))); return builder.build(); }
Builder is using same configuration interfaces behind the scenes that
the @Configuration
model using adapter classes. Same model goes to
configuring transitions, states and common configuration via builder’s
methods. This simply means that whatever you can use with a normal
EnumStateMachineConfigurerAdapter
or StateMachineConfigurerAdapter
can be used dynamically via a builder.
Note | |
---|---|
Currently |
StateMachine<String, String> buildMachine2() throws Exception { Builder<String, String> builder = StateMachineBuilder.builder(); builder.configureConfiguration() .withConfiguration() .autoStartup(false) .beanFactory(null) .taskExecutor(null) .taskScheduler(null) .listener(null); return builder.build(); }
It is important to understand on what cases common configuration needs
to be used with a machines instantiated from a builder. Configurer
returned from a withConfiguration()
can be used to setup autoStart,
TaskScheduler, TaskExecutor, BeanFactory and additionally register
a StateMachineListener. If StateMachine instance returned from
a builder is registered as a bean via @Bean
, i.e. BeanFactory
is attached automatically and then a default TaskExecutor can be found
from there. If instances are used outside of a spring application context
these methods must be used to setup needed facilities.
When an event is sent it may fire an EventTrigger
which then may cause
a transition to happen if a state machine is in a state where trigger is
evaluated successfully. Normally this may lead to a situation where
an event is not accepted and is dropped. However it may be desirable to
postpone this event until a state machine enters other state, in which
it is possible to accept that event. In other words an event simply
arrives at an inconvenient time.
Spring Statemachine provides a mechanism for deferring events for later processing. Every state can have a list of deferred events. If an event in the current state’s deferred event list occurs, the event will be saved (deferred) for future processing until a state is entered that does not list the event in its deferred event list. When such a state is entered, the state machine will automatically recall any saved events that are no longer deferred and will then either consume or discard these events. It is possible for a superstate to have a transition defined on an event that is deferred by a substate. Following same hierarchical state machines concepts, the substate takes precedence over the superstate, the event will be deferred and the transition for the superstate will not be executed. With orthogonal regions where one orthogonal region defers an event and another accepts the event, the accept takes precedence and the event is consumed and not deferred.
The most obvious use case for event deferring is when an event is causing a transition into a particular state and state machine is then returned back to its original state where second event should cause a same transition. Let’s take this with a simple example.
@Configuration @EnableStateMachine static class Config5 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineStateConfigurer<String, String> states) throws Exception { states .withStates() .initial("READY") .state("DEPLOYPREPARE", "DEPLOY") .state("DEPLOYEXECUTE", "DEPLOY"); } @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions .withExternal() .source("READY").target("DEPLOYPREPARE") .event("DEPLOY") .and() .withExternal() .source("DEPLOYPREPARE").target("DEPLOYEXECUTE") .and() .withExternal() .source("DEPLOYEXECUTE").target("READY"); } }
In above state machine has state READY which indicates that machine is ready to process events which would take it into a DEPLOY state where the actual deployment would happen. After deploy actions has been executed machine is then returned back into a READY state. Sending multiple events in a READY state is not causing any trouble if machine is using synchronous executor because event sending would block between event calls. However if executor is using threads then other events may get lost because machine is no longer in a state where event could be processed. Thus deferring some of these events allows machine to preserve these events.
@Configuration @EnableStateMachine static class Config6 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineStateConfigurer<String, String> states) throws Exception { states .withStates() .initial("READY") .state("DEPLOY", "DEPLOY") .state("DONE") .and() .withStates() .parent("DEPLOY") .initial("DEPLOYPREPARE") .state("DEPLOYPREPARE", "DONE") .state("DEPLOYEXECUTE"); } @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions .withExternal() .source("READY").target("DEPLOY") .event("DEPLOY") .and() .withExternal() .source("DEPLOYPREPARE").target("DEPLOYEXECUTE") .and() .withExternal() .source("DEPLOYEXECUTE").target("READY") .and() .withExternal() .source("READY").target("DONE") .event("DONE") .and() .withExternal() .source("DEPLOY").target("DONE") .event("DONE"); } }
In above state machine which is using nested states instead of a flat state model, event DEPLOY can be deferred directly in a substate. It is also showing concept of deferring event DONE in one of a sub-states which would then override anonymous transition between DEPLOY and DONE states if state machine happens to be in a DEPLOYPREPARE state when DONE event is dispatched. In DEPLOYEXECUTE state DONE event is not deferred, thus event would be handled in a super state.
Support for scopes in a state machine is very limited but it is possible
to enable use of session scope using a normal spring @Scope
annotation.
Firstly if state machine is build manually via a builder and returned into
context as @Bean
, and secondly via a configuration adapter. Both of
these simply needs an a @Scope
to be present where scopeName is set to
session and proxyMode to ScopedProxyMode.TARGET_CLASS
. Examples for
both use cases are shown below.
Tip | |
---|---|
See sample Chapter 45, Scope how to use session scoping. |
@Configuration public class Config3 { @Bean @Scope(scopeName="session", proxyMode=ScopedProxyMode.TARGET_CLASS) StateMachine<String, String> stateMachine() throws Exception { Builder<String, String> builder = StateMachineBuilder.builder(); builder.configureConfiguration() .withConfiguration() .autoStartup(true) .taskExecutor(new SyncTaskExecutor()); builder.configureStates() .withStates() .initial("S1") .state("S2"); builder.configureTransitions() .withExternal() .source("S1") .target("S2") .event("E1"); StateMachine<String, String> stateMachine = builder.build(); return stateMachine; } }
@Configuration @EnableStateMachine @Scope(scopeName="session", proxyMode=ScopedProxyMode.TARGET_CLASS) public static class Config4 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception { config .withConfiguration() .autoStartup(true); } @Override public void configure(StateMachineStateConfigurer<String, String> states) throws Exception { states .withStates() .initial("S1") .state("S2"); } @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions .withExternal() .source("S1") .target("S2") .event("E1"); } }
Once you have scoped state machine into session
, autowiring it into
a @Controller
will give new state machine instance per session.
State machine is then destroyed when HttpSession
is invalidated.
@Controller public class StateMachineController { @Autowired StateMachine<String, String> stateMachine; @RequestMapping(path="/state", method=RequestMethod.POST) public HttpEntity<Void> setState(@RequestParam("event") String event) { stateMachine.sendEvent(event); return new ResponseEntity<Void>(HttpStatus.ACCEPTED); } @RequestMapping(path="/state", method=RequestMethod.GET) @ResponseBody public String getState() { return stateMachine.getState().getId(); } }
Note | |
---|---|
Using state machines in a |
Note | |
---|---|
Spring Statemachine poms don’t have any dependencies to Spring MVC classes which you will need to work with session scope. But if you’re working with a web application, you’ve already pulled those deps directly from Spring MVC or Spring Boot. |
Actions are one of the most useful components from user perspective to interact and collaborate with a state machine. Actions can be executed in various places in a state machine and its states lifecycle like entering or exiting states or during a transitions.
@Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.SI) .state(States.S1, action1(), action2()) .state(States.S2, action1(), action2()) .state(States.S3, action1(), action3()); }
Above action1
and action2
beans are attached to states entry and
exit respectively.
@Bean public Action<States, Events> action1() { return new Action<States, Events>() { @Override public void execute(StateContext<States, Events> context) { } }; } @Bean public BaseAction action2() { return new BaseAction(); } @Bean public SpelAction action3() { ExpressionParser parser = new SpelExpressionParser(); return new SpelAction( parser.parseExpression( "stateMachine.sendEvent(T(org.springframework.statemachine.docs.Events).E1)")); } public class BaseAction implements Action<States, Events> { @Override public void execute(StateContext<States, Events> context) { } } public class SpelAction extends SpelExpressionAction<States, Events> { public SpelAction(Expression expression) { super(expression); } }
You can directly implement Action as an anonymous function or create a your own implementation and define appropriate implementation as a bean.
In action3
a SpEL expression is used to send event Events.E1 into
a state machine.
Note | |
---|---|
StateContext is described in section Chapter 19, Using StateContext. |
Above guard1
and guard2
beans are attached to states entry and
exit respectively.
@Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.SI).target(States.S1) .event(Events.E1) .guard(guard1()) .and() .withExternal() .source(States.S1).target(States.S2) .event(Events.E1) .guard(guard2()) .and() .withExternal() .source(States.S2).target(States.S3) .event(Events.E2) .guardExpression("extendedState.variables.get('myvar')"); }
You can directly implement Guard as an anonymous function or create
a your own implementation and define appropriate implementation as a
bean. In above sample guardExpression
is simply checking if extended
state variable myvar
evaluates to TRUE.
@Bean public Guard<States, Events> guard1() { return new Guard<States, Events>() { @Override public boolean evaluate(StateContext<States, Events> context) { return true; } }; } @Bean public BaseGuard guard2() { return new BaseGuard(); } public class BaseGuard implements Guard<States, Events> { @Override public boolean evaluate(StateContext<States, Events> context) { return false; } }
Note | |
---|---|
StateContext is described in section Chapter 19, Using StateContext. |
It is also possible to use SpEL expressions as a replacement for a full Guard implementation. Only requirement is that expression needs to return a Boolean value to satisfy Guard implementation. This is demonstrated with a guardExpression() function which takes an expression as an argument.
Let’s assume that we’d need to create a state machine tracking how many times a user is pressing a key on a keyboard and then terminate when keys are pressed 1000 times. Possible but a really naive solution would be to create a new state for each 1000 key presses. Going even worse combinations you might suddenly have astronomical number of states which naturally is not very practical.
This is where extended state variables comes into rescue by not having a necessity to add more states to drive state machine changes, instead a simple variable change can be done during a transition.
StateMachine
has a method getExtendedState()
which returns an
interface ExtendedState
which gives an access to extended state
variables. You can access variables directly via a state machine or
StateContext
during a callback from actions or transitions.
public Action<String, String> myVariableAction() { return new Action<String, String>() { @Override public void execute(StateContext<String, String> context) { context.getExtendedState() .getVariables().put("mykey", "myvalue"); } }; }
If there is a need to get notified for extended state variable
changes, there are two options; either use StateMachineListener
and
listen extendedStateChanged(key, value)
callbacks:
public class ExtendedStateVariableListener extends StateMachineListenerAdapter<String, String> { @Override public void extendedStateChanged(Object key, Object value) { // do something with changed variable } }
Or implement a Spring Application context listeners for
OnExtendedStateChanged
. Naturally as mentioned in Chapter 21, Listening State Machine Events
you can also listen all StateMachineEvent
events.
public class ExtendedStateVariableEventListener implements ApplicationListener<OnExtendedStateChanged> { @Override public void onApplicationEvent(OnExtendedStateChanged event) { // do something with changed variable } }
StateContext is a one of a most important objects when working with a state machine as it is passed into various methods and callbacks to give status of a current state of a state machine and where it is possibly going. If simplifying things a little it can be considered to be a snapshot of a current state machine stage where it is at a time StateContext is passed on.
Note | |
---|---|
In |
In overall StateContext can be used as.
Message
, Event
or their
MessageHeaders
if known.
Extended State
.
StateMachine
itself.
Transition
if applicable.
Stage
as described in Section 19.1, “Stages”.
StateContext is passed into various components interacting with user
like Action
and Guard
.
Stage is representation of a stage
on
which a state machine is currently interacting with a user. Current
stages are EVENT_NOT_ACCEPTED
, EXTENDED_STATE_CHANGED
,
STATE_CHANGED
, STATE_ENTRY
, STATE_EXIT
, STATEMACHINE_ERROR
,
STATEMACHINE_START
, STATEMACHINE_STOP
, TRANSITION
,
TRANSITION_START
and TRANSITION_END
which look very familiar as
those match how user can interact with listeners as described in
Chapter 21, Listening State Machine Events.
Driving a statemachine is done via transitions which are triggered by triggers. Currently supported triggers are EventTrigger and TimerTrigger.
EventTrigger is the most useful trigger because it allows user to directly interact with a state machine by sending events to it. These events are also called signals. Trigger is added to a transition simply by associating a state to it during a configuration.
@Autowired StateMachine<States, Events> stateMachine; void signalMachine() { stateMachine.sendEvent(Events.E1); Message<Events> message = MessageBuilder .withPayload(Events.E2) .setHeader("foo", "bar") .build(); stateMachine.sendEvent(message); }
In above example we send an event using two different ways. Firstly we
simply sent a type safe event using state machine api method
sendEvent(E event)
. Secondly we send event wrapped in a Spring
messaging Message using api method sendEvent(Message<E> message)
with a custom event headers. This allows user to add arbitrary extra
information with an event which is then visible to StateContext when
for example user is implementing actions.
Note | |
---|---|
Message headers are generally passed on until machine runs to
completion for a specific event. For example if an event is causing
transition into a state |
TimerTrigger is useful when something needs to be triggered
automatically without any user interaction. Trigger
is added to a
transition by associating a timer with it during a configuration.
Currently there are two types of timers supported, one which fires continuously and one which fires once a source state is entered.
@Configuration @EnableStateMachine public class Config2 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineStateConfigurer<String, String> states) throws Exception { states .withStates() .initial("S1") .state("S2") .state("S3"); } @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions .withExternal() .source("S1").target("S2").event("E1") .and() .withExternal() .source("S1").target("S3").event("E2") .and() .withInternal() .source("S2") .action(timerAction()) .timer(1000) .and() .withInternal() .source("S3") .action(timerAction()) .timerOnce(1000); } @Bean public TimerAction timerAction() { return new TimerAction(); } } public class TimerAction implements Action<String, String> { @Override public void execute(StateContext<String, String> context) { // do something in every 1 sec } }
In above we have three states, S1
, S2
and S3
. We have a normal
external transition from S1
to S2
and from S1
to S3
with
events E1
and E2
respectively. Interesting parts are when we define
internal transitions for source states S2
and S3
.
For both transitions we associate Action
bean timerAction
where
source state S2
will use timer
and S3
will use timerOnce
.
Values given are with milliseconds which in these cases mean 1000ms
.
Once a state machine receive event E1
it does a transition
from S1
to S2
and timer kicks in. As long as state is kept in S2
TimerTrigger
executes and causes a transition associated with that
state which in this case is the internal transition which has the
timerAction
defined.
Once a state machine receive event E2
it does a transition
from S1
to S3
and timer kicks in. This timer is executed only once
after state is entered after a delay defined in a timer.
Note | |
---|---|
Behind a scenes timers are a simple triggers which may cause an
transition to happen. Defining a transition with a |
Tip | |
---|---|
Use |
There are use cases where you just want to know what is happening with a state machine, react to something or simply get logging for debugging purposes. SSM provides interfaces for adding listeners which then gives an option to get callback when various state changes, actions, etc are happening.
You basically have two options, either to listen Spring application context events or directly attach listener to a state machine. Both of these basically will provide same information where one is producing events as event classes and other producing callbacks via a listener interface. Both of these have pros and cons which will be discussed later.
Application context events classes are OnTransitionStartEvent, OnTransitionEvent, OnTransitionEndEvent, OnStateExitEvent, OnStateEntryEvent, OnStateChangedEvent, OnStateMachineStart and OnStateMachineStop and others which extends base event class StateMachineEvent These can be used as is with spring typed ApplicationListener.
StateMachine will send context events via StateMachineEventPublisher it’s set. Default implementation is automatically created if @Configuration class is annotated with @EnableStateMachine.
public class StateMachineApplicationEventListener implements ApplicationListener<StateMachineEvent> { @Override public void onApplicationEvent(StateMachineEvent event) { } } @Configuration public class ListenerConfig { @Bean public StateMachineApplicationEventListener contextListener() { return new StateMachineApplicationEventListener(); } }
Context events are also automatically enabled via @EnableStateMachine with machine builder StateMachine registered as a bean as shown below.
@Configuration @EnableStateMachine public class ManualBuilderConfig { @Bean public StateMachine<String, String> stateMachine() throws Exception { Builder<String, String> builder = StateMachineBuilder.builder(); builder.configureStates() .withStates() .initial("S1") .state("S2"); builder.configureTransitions() .withExternal() .source("S1") .target("S2") .event("E1"); return builder.build(); } }
Using StateMachineListener you can either extend it and implement all callback methods or use StateMachineListenerAdapter class which contains stub method implementations and choose which ones to override.
public class StateMachineEventListener extends StateMachineListenerAdapter<States, Events> { @Override public void stateChanged(State<States, Events> from, State<States, Events> to) { } @Override public void stateEntered(State<States, Events> state) { } @Override public void stateExited(State<States, Events> state) { } @Override public void transition(Transition<States, Events> transition) { } @Override public void transitionStarted(Transition<States, Events> transition) { } @Override public void transitionEnded(Transition<States, Events> transition) { } @Override public void stateMachineStarted(StateMachine<States, Events> stateMachine) { } @Override public void stateMachineStopped(StateMachine<States, Events> stateMachine) { } @Override public void eventNotAccepted(Message<Events> event) { } @Override public void extendedStateChanged(Object key, Object value) { } @Override public void stateMachineError(StateMachine<States, Events> stateMachine, Exception exception) { } @Override public void stateContext(StateContext<States, Events> stateContext) { } }
In above example we simply created our own listener class StateMachineEventListener which extends StateMachineListenerAdapter.
Listener method stateContext
gives an access to various
StateContext changes on a different stages. More about about it in
section Chapter 19, Using StateContext.
Once you have your own listener defined, it can be registered into a state machine via its interface as shown below. It’s just a matter of flavour if it’s hooked up within a spring configuration or done manually at any time of application life-cycle.
public class Config7 { @Autowired StateMachine<States, Events> stateMachine; @Bean public StateMachineEventListener stateMachineEventListener() { StateMachineEventListener listener = new StateMachineEventListener(); stateMachine.addStateListener(listener); return listener; } }
Spring application context is not a fastest eventbus out there so it
is advised to give some thought what is a rate of events state machine
is sending. For better performance it may be better to use
StateMachineListener interface. For this specific reason it is
possible to use contextEvents
flag with @EnableStateMachine and
@EnableStateMachineFactory to disable Spring application context
events as shown above.
@Configuration @EnableStateMachine(contextEvents = false) public class Config8 extends EnumStateMachineConfigurerAdapter<States, Events> { } @Configuration @EnableStateMachineFactory(contextEvents = false) public class Config9 extends EnumStateMachineConfigurerAdapter<States, Events> { }
It is a little limited to do interaction with a state machine by either listening its events or using actions with states and transitions. Time to time this approach would be too limited and verbose to create interaction with the application a state machine is working with. For this specific use case we have made a spring style context integration which easily attach state machine functionality into your beans.
Available annotations has been harmonised to enable access to same state machine execution points than what is available from Chapter 21, Listening State Machine Events.
@WithStateMachine annotation can be used to associate a state machine with an existing bean. Then it is possible to start adding supported annotations to methods of that bean.
@WithStateMachine public class Bean1 { @OnTransition public void anyTransition() { } }
It is also possible to attach to any other state machine from an
application context by using annotation name
field.
@WithStateMachine(name = "myMachineBeanName") public class Bean2 { @OnTransition public void anyTransition() { } }
Sometimes it is more convenient to use machine id which is something user can set to better identify multiple instances. This id maps to getId() method in a StateMachine interface.
@WithStateMachine(id = "myMachineId") public class Bean16 { @OnTransition public void anyTransition() { } }
@WithStateMachine can also be used as a meta-annotation as shown above. In this case you could annotate your bean with WithMyBean.
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @WithStateMachine(name = "myMachineBeanName") public @interface WithMyBean { }
Note | |
---|---|
Return type of these methods doesn’t matter and is effectively discard. |
All features for @WithStateMachine can be enabled by using annotation @EnableWithStateMachine which simply imports needed configuration into Spring Application Context. Both @EnableStateMachine and @EnableStateMachineFactory are already annotated with this so there is no need for user to add it again. However if machine is build and configured without a use of configuration adapters, @EnableWithStateMachine must be used order to use features with @WithStateMachine. Idea for this is shown below.
public static StateMachine<String, String> buildMachine(BeanFactory beanFactory) throws Exception { Builder<String, String> builder = StateMachineBuilder.builder(); builder.configureConfiguration() .withConfiguration() .machineId("myMachineId") .beanFactory(beanFactory); builder.configureStates() .withStates() .initial("S1") .state("S2"); builder.configureTransitions() .withExternal() .source("S1") .target("S2") .event("E1"); return builder.build(); } @WithStateMachine(id = "myMachineId") static class Bean17 { @OnStateChanged public void onStateChanged() { } }
Important | |
---|---|
If machine is not created as a Bean then it is mandatory to set BeanFactory for a machine as shown above. Otherwise machine will be unaware of handlers calling your @WithStateMachine methods. |
Every annotation is supporting exactly same set of possible method parameters but runtime behaviour is different depending on an annotation itself and a stage where annotated method is called. To better understand how context works see Chapter 19, Using StateContext.
Note | |
---|---|
For differences between method parameters, see individual annotation docs below. |
Effectively all annotated methods are called using Spring SPel
expressions which are build dynamically during the process. As to make
this work these expressions needs to have a root object it evaluates
against. This root object is a StateContext
and we have also made some
tweaks internally so that it is possible to access StateContext
methods
directly without going through the context handle.
Simplest method parameter would naturally be a StateContext
itself.
@WithStateMachine public class Bean3 { @OnTransition public void anyTransition(StateContext<String, String> stateContext) { } }
Rest of the StateContext
content can be accessed as shown below.
Number of parameters or order of those doesn’t matter.
@WithStateMachine public class Bean4 { @OnTransition public void anyTransition( @EventHeaders Map<String, Object> headers, @EventHeader("myheader1") Object myheader1, @EventHeader(name = "myheader2", required = false) String myheader2, ExtendedState extendedState, StateMachine<String, String> stateMachine, Message<String> message, Exception e) { } }
Note | |
---|---|
Instead of getting all event headers with |
Annotations for transitions are OnTransition
, OnTransitionStart
and OnTransitionEnd
.
These annotations behave exactly same and let’s
see how OnTransition
is used. Within this annotation a property’s
source and target can be used to qualify a transition. If
source and target is left empty then any transition is matched.
@WithStateMachine public class Bean5 { @OnTransition(source = "S1", target = "S2") public void fromS1ToS2() { } @OnTransition public void anyTransition() { } }
Default @OnTransition annotation can’t be used with a state and event enums user have created due to java language limitations, thus string representation have to be used.
Additionally it is possible to access Event Headers
and
ExtendedState
by adding needed arguments to a method. Method
is then called automatically with these arguments.
@WithStateMachine public class Bean6 { @StatesOnTransition(source = States.S1, target = States.S2) public void fromS1ToS2(@EventHeaders Map<String, Object> headers, ExtendedState extendedState) { } }
However if you want to have a type safe annotation it is possible to create a new annotation and use @OnTransition as meta annotation. This user level annotation can make a reference to actual states and events enums and framework will try to match these in a same way.
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @OnTransition public @interface StatesOnTransition { States[] source() default {}; States[] target() default {}; }
Above we created a @StatesOnTransition annotation which defines
source
and target
as a type safe manner.
@WithStateMachine public class Bean7 { @StatesOnTransition(source = States.S1, target = States.S2) public void fromS1ToS2() { } }
In your own bean you can then use this @StatesOnTransition as is and
use type safe source
and target
.
Annotations for states are OnStateChanged
, OnStateEntry
and
OnStateExit
.
@WithStateMachine public class Bean8 { @OnStateChanged public void anyStateChange() { } }
In a same way that in transition annotations it’s possible to define target and source states.
@WithStateMachine public class Bean9 { @OnStateChanged(source = "S1", target = "S2") public void stateChangeFromS1toS2() { } }
For type safety a new annotation needs to be created for enums with
OnStateChanged
as a meta annotation.
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @OnStateChanged public @interface StatesOnStates { States[] source() default {}; States[] target() default {}; }
@WithStateMachine public class Bean10 { @StatesOnStates(source = States.S1, target = States.S2) public void fromS1ToS2() { } }
Methods for state entry and exit behave in a same way.
@WithStateMachine public class Bean11 { @OnStateEntry public void anyStateEntry() { } @OnStateExit public void anyStateExit() { } }
There is one event related annotation named OnEventNotAccepted
. It
is possible to listen only specific event by defining event
property
with the annotation.
@WithStateMachine public class Bean12 { @OnEventNotAccepted public void anyEventNotAccepted() { } @OnEventNotAccepted(event = "E1") public void e1EventNotAccepted() { } }
Annotations for state machine are OnStateMachineStart
,
OnStateMachineStop
and OnStateMachineError
.
During a state machine start and stop lifecycle methods are called.
@WithStateMachine public class Bean13 { @OnStateMachineStart public void onStateMachineStart() { } @OnStateMachineStop public void onStateMachineStop() { } }
In case a state machine goes into an error with exception, below annotation is called.
@WithStateMachine public class Bean14 { @OnStateMachineError public void onStateMachineError() { } }
There is one extended state related annotation named
OnExtendedStateChanged
. It’s also possible to listen changes only
for specific key
changes.
@WithStateMachine public class Bean15 { @OnExtendedStateChanged public void anyStateChange() { } @OnExtendedStateChanged(key = "key1") public void key1Changed() { } }
StateMachine
is a main interface to communicate with a state machine
itself. Time to time there is a need to get more dynamical and
programmatic access to internal structures of a state machine and its
nested machines and regions. For these use cases a StateMachine
is
exposing a functional interface StateMachineAccessor
which provides
an interface to get access to individual StateMachine
and
Region
instances.
StateMachineFunction
is a simple functional interface which allows
to apply StateMachineAccess
interface into a state machine. With
jdk7 these will create a little verbose code but with jdk8 lambdas
things look relatively non-verbose.
Method doWithAllRegions
gives access to all Region
instances in
a state machine.
stateMachine.getStateMachineAccessor().doWithAllRegions(new StateMachineFunction<StateMachineAccess<String,String>>() { @Override public void apply(StateMachineAccess<String, String> function) { function.setRelay(stateMachine); } }); stateMachine.getStateMachineAccessor() .doWithAllRegions(access -> access.setRelay(stateMachine));
Method doWithRegion
gives access to single Region
instance in a
state machine.
stateMachine.getStateMachineAccessor().doWithRegion(new StateMachineFunction<StateMachineAccess<String,String>>() { @Override public void apply(StateMachineAccess<String, String> function) { function.setRelay(stateMachine); } }); stateMachine.getStateMachineAccessor() .doWithRegion(access -> access.setRelay(stateMachine));
Method withAllRegions
gives access to all Region
instances in
a state machine.
for (StateMachineAccess<String, String> access : stateMachine.getStateMachineAccessor().withAllRegions()) {
access.setRelay(stateMachine);
}
stateMachine.getStateMachineAccessor().withAllRegions()
.stream().forEach(access -> access.setRelay(stateMachine));
Method withRegion
gives access to single Region
instance in a
state machine.
stateMachine.getStateMachineAccessor() .withRegion().setRelay(stateMachine);
Instead of using a StateMachineListener
interface one option is to
use a StateMachineInterceptor
. One conceptual difference is that an
interceptor can be used to intercept and stop a current state
change or transition logic. Instead of implementing full interface,
adapter class StateMachineInterceptorAdapter
can be used to override
default no-op methods.
Note | |
---|---|
There is one recipe Chapter 35, Persist and one sample Chapter 42, Persist which are related to use of an interceptor. |
Interceptor can be registered via StateMachineAccessor
. Concept of
an interceptor is relatively deep internal feature and thus is not
exposed directly via StateMachine
interface.
stateMachine.getStateMachineAccessor() .withRegion().addStateMachineInterceptor(new StateMachineInterceptor<String, String>() { @Override public Message<String> preEvent(Message<String> message, StateMachine<String, String> stateMachine) { return message; } @Override public StateContext<String, String> preTransition(StateContext<String, String> stateContext) { return stateContext; } @Override public void preStateChange(State<String, String> state, Message<String> message, Transition<String, String> transition, StateMachine<String, String> stateMachine) { } @Override public void preStateChange(State<String, String> state, Message<String> message, Transition<String, String> transition, StateMachine<String, String> stateMachine, StateMachine<String, String> rootStateMachine) { } @Override public StateContext<String, String> postTransition(StateContext<String, String> stateContext) { return stateContext; } @Override public void postStateChange(State<String, String> state, Message<String> message, Transition<String, String> transition, StateMachine<String, String> stateMachine) { } @Override public void postStateChange(State<String, String> state, Message<String> message, Transition<String, String> transition, StateMachine<String, String> stateMachine, StateMachine<String, String> rootStateMachine) { } @Override public Exception stateMachineError(StateMachine<String, String> stateMachine, Exception exception) { return exception; } });
Note | |
---|---|
More about error handling shown in above example, see section Chapter 26, State Machine Error Handling. |
Security features are built atop of functionality from a Spring Security. Security features are handy when it is required to protect part of a state machine execution and interaction with it.
Important | |
---|---|
We expect user to be fairly familiar with a Spring Security meaning we don’t go into details of how overall security framework works. For this read Spring Security reference documentation. |
First level of defence with a security is naturally protecting events which really are a driver from user point of view what is going to happen in a state machine. More fine grained security settings can then be defined for transitions and actions. This can be think of like allowing an employee to access a building, walk around it and then giving more detailed access rights to enter different rooms and allow to switch lights on and off while being on those rooms. If you trust your users then event security may be all you need, if you don’t, then more detailed security needs to be applied.
More detailed info can be found from section Section 25.6, “Understanding Security”.
Tip | |
---|---|
For complete example, see sample Chapter 46, Security. |
All generic configurations for security are done in
SecurityConfigurer
which is obtained from
StateMachineConfigurationConfigurer
. Security is disabled on
default even if Spring Security classes are
present.
@Configuration @EnableStateMachine static class Config4 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception { config .withSecurity() .enabled(true) .transitionAccessDecisionManager(null) .eventAccessDecisionManager(null); } }
If absolutely needed AccessDecisionManager
for both events and
transitions can be customised. If decision managers are not defined or
are set to null
, default managers are created internally.
Event security is defined on a global level within a
SecurityConfigurer
.
@Configuration @EnableStateMachine static class Config1 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception { config .withSecurity() .enabled(true) .event("true") .event("ROLE_ANONYMOUS", ComparisonType.ANY); } }
In above configuration we use expression true
which always evaluates
to TRUE. Using an expression which always evaluates to TRUE
would not make sense in a real application but gives a point that
expression needs to return either TRUE or FALSE. We also defined
attribute ROLE_ANONYMOUS
and ComparisonType
ANY
. Using attributes
and expressions, see section Section 25.5, “Using Security Attributes and Expressions”.
Transition security can be defined globally.
@Configuration @EnableStateMachine static class Config6 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception { config .withSecurity() .enabled(true) .transition("true") .transition("ROLE_ANONYMOUS", ComparisonType.ANY); } }
If security is defined in a transition itself it will override any globally set security.
@Configuration @EnableStateMachine static class Config2 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions .withExternal() .source("S0") .target("S1") .event("A") .secured("ROLE_ANONYMOUS", ComparisonType.ANY) .secured("hasTarget('S1')"); } }
Using attributes and expressions, see section Section 25.5, “Using Security Attributes and Expressions”.
There are no dedicated security definitions for actions in a state
machine, but it can be accomplished using a global method security
from a Spring Security. This simply needs that an Action
is
defined as a proxied @Bean
and its execute
method annotated with a
@Secured
.
@Configuration @EnableStateMachine static class Config3 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception { config .withSecurity() .enabled(true); } @Override public void configure(StateMachineStateConfigurer<String, String> states) throws Exception { states .withStates() .initial("S0") .state("S1"); } @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions .withExternal() .source("S0") .target("S1") .action(securedAction()) .event("A"); } @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) @Bean public Action<String, String> securedAction() { return new Action<String, String>() { @Secured("ROLE_ANONYMOUS") @Override public void execute(StateContext<String, String> context) { } }; } }
Global method security needs to be enabled with a Spring Security which is done with along a lines shown below. See Spring Security reference docs for more details.
@Configuration @EnableGlobalMethodSecurity(securedEnabled = true) public static class Config5 extends WebSecurityConfigurerAdapter { @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user").password("password").roles("USER"); } }
Generally there are two ways to define security properties, firstly using security attributes and secondly using security expressions. Attributes are easier to use but are relatively limited in terms of functionality. Expressions provide more features but are a little bit of harder to use.
On default AccessDecisionManager
instances for events and
transitions both use a RoleVoter
, meaning you can use role attributes
familiar from Spring Security.
For attributes we have 3 different comparison types, ANY
, ALL
and
MAJORITY
which maps into default access decision managers
AffirmativeBased
, UnanimousBased
and ConsensusBased
respectively.
If custom AccessDecisionManager
has been defined, comparison type is
effectively discarded as it’s only used to create a default manager.
Security expressions needs to return either TRUE or FALSE.
The base class for expression root objects is
SecurityExpressionRoot
. This provides some common expressions which
are available in both transition and event security.
Table 25.1. Common built-in expressions
Expression | Description |
---|---|
| Returns |
| Returns |
| Returns |
| Returns |
| Allows direct access to the principal object representing the current user |
| Allows direct access to the current |
| Always evaluates to |
| Always evaluates to |
| Returns |
| Returns |
| Returns |
| Returns |
| Returns |
| Returns |
Event id can be matched by using prefix EVENT_
. For example matching
event A
would match with attribute EVENT_A
.
The base class for expression root object for event is
EventSecurityExpressionRoot
. This provides access to a Message
object which is passed around with eventing.
Table 25.2. Event expressions
Expression | Description |
---|---|
| Returns |
Matching transition sources and targets, use prefixes
TRANSITION_SOURCE_
and TRANSITION_TARGET_
respectively.
The base class for expression root object for transition is
TransitionSecurityExpressionRoot
. This provides access to a
Transition
object which is passed around for transition changes.
Table 25.3. Transition expressions
Expression | Description |
---|---|
| Returns |
| Returns |
This section provides more detailed info how security works within a state machine. Not really something you’d need to know but it is always better to be transparent instead of hiding all the magic what happens behind the scenes.
Note | |
---|---|
Security only makes sense if State Machine is executed in a walled
garden where user don’t have direct access to the application thus
could modify Spring Security’s |
Integration point for security is done with a
StateMachineInterceptor which is then added automatically into a
state machine if security is enabled. Specific class is a
StateMachineSecurityInterceptor
which intercepts events and
transitions. This interceptor then consults Spring Security’s
AccessDecisionManager
if event can be send or if transition can be
executed. Effectively if decision or vote with a AccessDecisionManager
will result an exception, event or transition is denied.
Due to way how AccessDecisionManager
from Spring Security works, we
need one instance of it per secured object. This is a reason why there
is a different manager for events and transitions. In this case events
and transitions are different class objects we’re securing.
On default for events, voters EventExpressionVoter
, EventVoter
and
RoleVoter
are added into a AccessDecisionManager
.
On default for transitions, voters TransitionExpressionVoter
,
TransitionVoter
and RoleVoter
are added into a AccessDecisionManager
.
If state machine detects an internal error during a state transition logic it may throw an exception. Before this exception is processed internally, user is given a chance to intercept.
Normal StateMachineInterceptor
can be used to intercept errors and
example of it is shown above.
StateMachine<String, String> stateMachine; void addInterceptor() { stateMachine.getStateMachineAccessor() .doWithRegion(new StateMachineFunction<StateMachineAccess<String, String>>() { @Override public void apply(StateMachineAccess<String, String> function) { function.addStateMachineInterceptor( new StateMachineInterceptorAdapter<String, String>() { @Override public Exception stateMachineError(StateMachine<String, String> stateMachine, Exception exception) { // return null indicating handled error return exception; } }); } }); }
When errors are detected, normal event notify mechanism is executed.
This allows to use either StateMachineListener
or Spring Application
context event listener, more about these read section
Chapter 21, Listening State Machine Events.
Having said that, a simple listener would look like:
public class ErrorStateMachineListener extends StateMachineListenerAdapter<String, String> { @Override public void stateMachineError(StateMachine<String, String> stateMachine, Exception exception) { // do something with error } }
Generic ApplicationListener
checking StateMachineEvent
would look
like.
public class GenericApplicationEventListener implements ApplicationListener<StateMachineEvent> { @Override public void onApplicationEvent(StateMachineEvent event) { if (event instanceof OnStateMachineError) { // do something with error } } }
It’s also possible to define ApplicationListener
directly to
recognize only StateMachineEvent
instances.
public class ErrorApplicationEventListener implements ApplicationListener<OnStateMachineError> { @Override public void onApplicationEvent(OnStateMachineError event) { // do something with error } }
Tip | |
---|---|
Actions defined for transitions also have their own error handling logic Section 11.7.2, “Transition Action Error Handling”. |
StateMachine services are higher level implementations meant to provide more user level functionalities to ease normal runtime operations. Currently only one service interface Section 27.1, “Using StateMachineService” exists.
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 35, Persist and one sample Chapter 42, 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 24, 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.
StateMachineContext
contains two different ways to include information
for a child contexts. These are generally used when machine contains
orthogonal regions. Firstly context can have a list of child context
as is which takes a presence if exists, secondly it is possible
to include a list of references which are used if raw context childs
are not in place. These child references are really only way to
persist a machine where multiple parallel regions are running
independently.
Tip | |
---|---|
There is a sample Chapter 52, Data Multi Persist showing how parallel regions can be persisted. |
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 47, 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.
StateMachineRuntimePersister
is a simple extension to
StateMachinePersist
adding interface level method to get
StateMachineInterceptor
associated with it. This interceptor is then
required to persist machine during state changes without needing to
stop and start a machine.
Currently there are implementations for this interface for out-of-the-box
supported Spring Data Repositories. These are
JpaPersistingStateMachineInterceptor
, MongoDbPersistingStateMachineInterceptor
and RedisPersistingStateMachineInterceptor
.
Tip | |
---|---|
Check sample Chapter 51, Data Persist for detailed usage. |
Auto-configuration module spring-statemachine-autoconfigure
contains all
integration logic with Spring Boot providing functionality i.e. for
auto-config and actuators. All what is needed is to have State
Machine as part of a boot application together with this library.
BootStateMachineMonitor
is created automatically and associated with
a state machine. BootStateMachineMonitor
is a custom StateMachineMonitor
implementation which integrates with boot’s MeterRegistry
and endpoints
via a custom StateMachineTraceRepository
. Optionally this auto-configuration
can be disabled by setting key spring.statemachine.monitor.enabled
to
false
. Use of this auto-config is shown in sample
Chapter 53, Monitoring.
Spring Data Repositories and Entity class scanning is auto-configured automatically for Chapter 34, Repository Support if needed classes are found from a classpath.
Currently supported configs are configured for JPA
, Redis
and
MongoDB
. Repository auto-configuration can be disabled using a
properties spring.statemachine.data.jpa.repositories.enabled
,
spring.statemachine.data.redis.repositories.enabled
and
spring.statemachine.data.mongo.repositories.enabled
respectively.
StateMachineMonitor
can be used to get more information about
durations of how long transitions and actions takes to execute. Below
you can see how this interface is implemented.
public class TestStateMachineMonitor extends AbstractStateMachineMonitor<String, String> { @Override public void transition(StateMachine<String, String> stateMachine, Transition<String, String> transition, long duration) { } @Override public void action(StateMachine<String, String> stateMachine, Action<String, String> action, long duration) { } }
Once you have StateMachineMonitor
implementation it can be added to
a state machine via configuration as shown below.
@Configuration @EnableStateMachine public class Config1 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception { config .withMonitoring() .monitor(stateMachineMonitor()); } @Override public void configure(StateMachineStateConfigurer<String, String> states) throws Exception { states .withStates() .initial("S1") .state("S2"); } @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions .withExternal() .source("S1") .target("S2") .event("E1"); } @Bean public StateMachineMonitor<String, String> stateMachineMonitor() { return new TestStateMachineMonitor(); } }
Tip | |
---|---|
Check sample Chapter 53, Monitoring for detailed usage. |
Distributed state is probably one of a most complicated concepts of a Spring State Machine. What exactly is a distributed state? A state within a single state machine is naturally really simple to understand but when there is a need to introduce a shared distributed state through a state machines, things will get a little complicated.
Note | |
---|---|
Distributed state functionality is still a preview feature and is not yet considered to be stable in this particular release. We expect this feature to mature towards the first official release. |
For generic configuration support see section Section 11.9, “Configuring Common Settings” and actual usage example see sample Chapter 43, Zookeeper.
Distributed State Machine
is implemented via a
DistributedStateMachine
class which simply wraps an actual instance
of a StateMachine
. DistributedStateMachine
intercepts
communication with a StateMachine
instance and works with
distributed state abstractions handled via interface
StateMachineEnsemble
. Depending on an actual implementation
StateMachinePersist
interface may also be used to serialize a
StateMachineContext
which contains enough information to reset a
StateMachine
.
While Distributed State Machine
is implemented via an abstraction,
only one implementation currently exists based on Zookeeper
.
Here is a generic example of how Zookeeper
based Distributed State
Machine
would be configured.
@Configuration @EnableStateMachine public class Config extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception { config .withDistributed() .ensemble(stateMachineEnsemble()) .and() .withConfiguration() .autoStartup(true); } @Override public void configure(StateMachineStateConfigurer<String, String> states) throws Exception { // config states } @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { // config transitions } @Bean public StateMachineEnsemble<String, String> stateMachineEnsemble() throws Exception { return new ZookeeperStateMachineEnsemble<String, String>(curatorClient(), "/zkpath"); } @Bean public CuratorFramework curatorClient() throws Exception { CuratorFramework client = CuratorFrameworkFactory .builder() .defaultData(new byte[0]) .connectString("localhost:2181").build(); client.start(); return client; } }
Current technical documentation of a Zookeeker
based distributed
state machine can be found from an appendice Appendix C, Distributed State Machine Technical Paper.
ZookeeperStateMachineEnsemble
itself needs two mandatory settings,
an instance of curatorClient
and basePath
. Client is a
CuratorFramework
and path is root of a tree in a Zookeeper
.
Optionally it is possible to set cleanState
which defaults to TRUE
and will clear existing data if no members exists in an ensemble. Set
this to FALSE
if you want to preserve distributed state within
application restarts.
Optionally it is possible to set a size of a logSize
which defaults
to 32
and is used to keep history of state changes. Value of this
setting needs to be a power of two. 32
is generally good default
value but if a particular state machine is left behind more than a
size of a log it is put into error state and disconnected from an
ensemble indicating it has lost its history to reconstruct fully
synchronized status.
We have also added a set of utility classes to easy testing of a state machine instances. These are used in a framework itself but are also very useful for end users.
StateMachineTestPlanBuilder
is used to build a StateMachineTestPlan
which then have one method test()
which runs a plan.
StateMachineTestPlanBuilder
contains a fluent builder api to add
steps into a plan and during these steps you can send events and check
various conditions like state changes, transitions and extended state
variables.
Let’s take a simple StateMachine
build using below example:
private StateMachine<String, String> buildMachine() throws Exception { StateMachineBuilder.Builder<String, String> builder = StateMachineBuilder.builder(); builder.configureConfiguration() .withConfiguration() .taskExecutor(new SyncTaskExecutor()) .autoStartup(true); builder.configureStates() .withStates() .initial("SI") .state("S1"); builder.configureTransitions() .withExternal() .source("SI").target("S1") .event("E1") .action(c -> { c.getExtendedState().getVariables().put("key1", "value1"); }); return builder.build(); }
In below test plan we have two steps, first we check that initial
state SI
is indeed set, secondly we send an event E1
and expect
one state change to happen and machine to end up into a state S1
.
StateMachine<String, String> machine = buildMachine(); StateMachineTestPlan<String, String> plan = StateMachineTestPlanBuilder.<String, String>builder() .defaultAwaitTime(2) .stateMachine(machine) .step() .expectStates("SI") .and() .step() .sendEvent("E1") .expectStateChanged(1) .expectStates("S1") .expectVariable("key1") .expectVariable("key1", "value1") .expectVariableWith(hasKey("key1")) .expectVariableWith(hasValue("value1")) .expectVariableWith(hasEntry("key1", "value1")) .expectVariableWith(not(hasKey("key2"))) .and() .build(); plan.test();
These utilities are also used within a framework to test distributed state machine features and multiple machines can be added to a plan. If multiple machines are added then it is also possible to choose if event is sent to particular, random or all machines.
Above testing example uses hamcrest imports:
import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.collection.IsMapContaining.hasKey; import static org.hamcrest.collection.IsMapContaining.hasValue; import static org.hamcrest.collection.IsMapContaining.hasEntry;
Tip | |
---|---|
All possible options for expected are documented in javadocs StateMachineTestPlanStepBuilder. |
Defining a state machine configuration with UI modeling is supported via Eclipse Papyrus framework.
From eclipse wizard create a new Papyrus Model with UML
Diagram
Language. In this example it’s named as simple-machine
. Then you’ve
given an option to choose various diagram kind’s and a StateMachine
Diagram
must be chosen.
We want to create a machine having two states, S1
and S2
where
S1
is initial state. Then event E1
is created to do a transition
from S1
to S2
. In papyrus a machine would then look like something
shown below.
Behind a scenes a raw uml file would look like.
<?xml version="1.0" encoding="UTF-8"?> <uml:Model xmi:version="20131001" xmlns:xmi="http://www.omg.org/spec/XMI/20131001" xmlns:uml="http://www.eclipse.org/uml2/5.0.0/UML" xmi:id="_AMP3IP8fEeW45bORGB4c_A" name="RootElement"> <packagedElement xmi:type="uml:StateMachine" xmi:id="_AMRFQP8fEeW45bORGB4c_A" name="StateMachine"> <region xmi:type="uml:Region" xmi:id="_AMRsUP8fEeW45bORGB4c_A" name="Region1"> <transition xmi:type="uml:Transition" xmi:id="_chgcgP8fEeW45bORGB4c_A" source="_EZrg4P8fEeW45bORGB4c_A" target="_FAvg4P8fEeW45bORGB4c_A"> <trigger xmi:type="uml:Trigger" xmi:id="_hs5jUP8fEeW45bORGB4c_A" event="_NeH84P8fEeW45bORGB4c_A"/> </transition> <transition xmi:type="uml:Transition" xmi:id="_egLIoP8fEeW45bORGB4c_A" source="_Fg0IEP8fEeW45bORGB4c_A" target="_EZrg4P8fEeW45bORGB4c_A"/> <subvertex xmi:type="uml:State" xmi:id="_EZrg4P8fEeW45bORGB4c_A" name="S1"/> <subvertex xmi:type="uml:State" xmi:id="_FAvg4P8fEeW45bORGB4c_A" name="S2"/> <subvertex xmi:type="uml:Pseudostate" xmi:id="_Fg0IEP8fEeW45bORGB4c_A"/> </region> </packagedElement> <packagedElement xmi:type="uml:Signal" xmi:id="_L01D0P8fEeW45bORGB4c_A" name="E1"/> <packagedElement xmi:type="uml:SignalEvent" xmi:id="_NeH84P8fEeW45bORGB4c_A" name="SignalEventE1" signal="_L01D0P8fEeW45bORGB4c_A"/> </uml:Model>
Tip | |
---|---|
When opening existing uml model defined as uml, you’ll have three
files, |
After uml file is in place in your project, it can be imported into
configuration using StateMachineModelConfigurer
where
StateMachineModelFactory
is associated with a model.
UmlStateMachineModelFactory
is a special factory which knows how to
process Eclipse Papyrus generated uml structure. Source uml file can
either be given as a Spring Resource
or a normal location string.
@Configuration @EnableStateMachine public static class Config1 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineModelConfigurer<String, String> model) throws Exception { model .withModel() .factory(modelFactory()); } @Bean public StateMachineModelFactory<String, String> modelFactory() { return new UmlStateMachineModelFactory("classpath:org/springframework/statemachine/uml/docs/simple-machine.uml"); } }
As usually Spring StateMachine is working with Guards and Actions which are defined as bean, those need to be hooked into uml by its internal modeling structure. In a below sections you will see how customized bean references are defined within uml definitions. Thought it is also possible to register particular methods manually without defining those as beans.
If UmlStateMachineModelFactory
is created as a bean its
ResourceLoader
is wired automatically to find registered actions and
guards. It’s also possible to manually define a
StateMachineComponentResolver
which will then be used to find these
components. Factory also have methods registerAction and
registerGuard which can be used to register these components. More
about this in Section 33.1.1, “StateMachineComponentResolver”.
Uml model is relatively loose what comes for the implementation like Spring StateMachine itself. There are choices what implementation need to take for uml support as it leaves a lot of features and functionalities for an implementation to decide. Below sections go through how Spring StateMachine will implement uml model based on Eclipse Papyrus plugin.
Below example shows how UmlStateMachineModelFactory
is defined with
a StateMachineComponentResolver
which registers a simple functions
myAction
and myGuard
respectively. As you notice these components
are not created as beans.
@Configuration @EnableStateMachine public static class Config2 extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineModelConfigurer<String, String> model) throws Exception { model .withModel() .factory(modelFactory()); } @Bean public StateMachineModelFactory<String, String> modelFactory() { UmlStateMachineModelFactory factory = new UmlStateMachineModelFactory( "classpath:org/springframework/statemachine/uml/docs/simple-machine.uml"); factory.setStateMachineComponentResolver(stateMachineComponentResolver()); return factory; } @Bean public StateMachineComponentResolver<String, String> stateMachineComponentResolver() { DefaultStateMachineComponentResolver<String, String> resolver = new DefaultStateMachineComponentResolver<>(); resolver.registerAction("myAction", myAction()); resolver.registerGuard("myGuard", myGuard()); return resolver; } public Action<String, String> myAction() { return new Action<String, String>() { @Override public void execute(StateContext<String, String> context) { } }; } public Guard<String, String> myGuard() { return new Guard<String, String>() { @Override public boolean evaluate(StateContext<String, String> context) { return false; } }; } }
Let’s start by creating an empty state machine model.
You’ll start by creating a new model and giving it a name.
Then you need to choose a StateMachine Diagram.
You end up having an empty state machine.
In above sample named model
you’ll end up three files, model.di
,
model.notation
and model.uml
which can then be used in any other
eclipse instance and model.uml
can be used by importing it into a
Spring Statemachine.
State identifier is simply coming from a component name in a diagram. You must have initial state in your machine which is done by adding Initial and then drawing a transition to your own initial state.
In above we added one state S1
, initial state, and draw a transition
between those two to indicate that S1
is an initial state.
In above we added a second state S2
and added a transition between
those two.
To associate an event for a transition you need to create a Signal
E1
. Done from RootElement→New Child→Signal.
And then SignalEvent
with defined signal E1
. Done from
RootElement→New Child→SignalEvent.
Now that you have a SignalEvent
defined it can be used to associate
a trigger with a transition. More about that in
Section 33.5, “Define Transitions”.
Transition is simply created by drawing transition line between
source and target states. In above we have states S1
and S2
and
anonymous transition between those two. We want to associate event
E1
with that transition. We choose a transition, create a new
trigger and define SignalEventE1
for that.
This will give you something like shown below.
Tip | |
---|---|
If |
Transition can also happen based on timed events. Spring Statemachine support two types of timers, ones which fires continuously on a background and ones which fires once with a delay when state is entered.
Add new TimeEvent
child to Model Explorer, modify When as
expression defined as LiteralInteger. Value of it is then timer as
milliseconds. Is Relative is left to false making timer to fire
continuously.
To define one timed based event when state is entered it’s exactly same as above but Is Relative is now defined as true.
Then what is left for user is to pick these time events instead of signal event for a particular transition.
Choice is simply defined by drawing one incoming transition into a
CHOICE
states and multiple outgoing transition from it into target
states. Configuration model in our StateConfigurer
allows to define
if/elseif/else structure but with uml we simply need to work with
individual Guards for outgoing transitions.
Make sure that guards defined for transitions do not overlap so that whatever happens, only one guard would evaluate to TRUE at any given time. This gives precise and predictable results for choice branch evaluation. Also it is advised to leave one transition without a guard so that at least one transition path is guaranteed.
Note | |
---|---|
Junction is very much same except it allows multiple incoming transitions. Thus its behaviour compared to choice is purely academic. Actual logic to select outgoing transition is exactly same. |
EntryPoint and ExitPoint are used to do controlled entry and exit
with state having sub-states. In a below statechart events E1
and
E2
will do a normal state behaviour by entering and exiting state
S2
where normal state behaviour happens by entering initial state
S21
.
Using event E3
takes machine into EntryPoint ENTRY
which then
leads into S22
without activating initial state S21
at any time.
Similarly ExitPoint EXIT
with event E4
controls specific exit
into state S4
while normal exit behaviour from S2
would take
machine into state S3
. While being on a state S22
you can choose
events E4
or E2
to take machine into states S3
or S4
respectively.
Note | |
---|---|
If state is defined as submachine reference and entry/exit points need to be used, a ConnectionPointReference has to be defined externally , its entry/exit reference set to point to a correct entry/exit point within a submachine reference. Only after that it is possible to target a transition which correctly links from outside into inside of a submachine reference. With ConnectionPointReference you may need to find these settings from Properties → Advanced → UML → Entry/Exit. UML Spec allows to define multiple entries and exits but with a state machine only one is allowed. |
When working with history states three different concepts are in play. UML defines a Deep History and a Shallow History. Default History State comes into play when history state is not yet known. These are represented in following sections.
Deep History is used for state which has other deep nested states, thus giving a chance to save whole nested state structure.
In cases where a Transition terminates on a history when
the state has not been entered before or it had reached its
final state, there is an option to force
a transition to a specific substate, using the default
history mechanism. For this to happen you simply define transition
into this default state. This is would be a transition from SH
to
S22
.
In a below example state S22
would be entered if state S2
has
never been active as its history has never been recorded. If state
S2
has been active then either S20
or S21
would get chosen.
Both Fork and Join are represented as bars in Papyrus. As shown
below you need to draw one outgoing transition from FORK
into state
S2
which have orthogonal regions. JOIN
is then reverse where
joined states are collected together via incoming transitions.
State entry and exit actions can be associated by using a behaviour, more about this in Section 33.14, “Define Bean Reference”.
Initial action as shown in Section 11.7, “Configuring Actions” is defined in uml by adding action in transition leading from Initial State marker into actual state. This Action is then executed when state machine is started.
Guard can be defined by first adding Constraint and then defining its Specification as OpaqueExpression which works in a same way than Section 33.14, “Define Bean Reference”.
When there is a need to make a bean reference in any uml effect,
action or guard, supported method to do that is via
FunctionBehavior
or OpaqueBehavior
where defined language needs to
be bean
and language body having a bean reference id.
When there is a need to use a SpEL instead of a bean reference in
any uml effect, action or guard, supported method to do that is via
FunctionBehavior
or OpaqueBehavior
where defined language needs to
be spel
and language body having a SpEL expression.
Normally when using sub-states those are simply drawn into a state chart itself. Chart itself may become a little complex and big to follow so we also support defining sub-state as a statemachine reference.
First create a New Diagram and give it a name i.e. SubStateMachine Diagram.
Give new diagram a design you need.
From state you want to link(in this case state S2
), click
Submachine
field and choose your linked machine, i.e.
SubStateMachine.
Finally you’ll see that state S2
is linked to SubStateMachine
as a
sub-state.
This section contains documentation related to using 'Spring Data Repositories' used in State Machine.
It is also possible to keep machine configuration in an external storage where it will be loaded on demand instead of creating a static configuration either using JavaConfig or UML based config. This integration works via Spring Data Repository abstraction.
We have created special StateMachineModelFactory
implementation
called RepositoryStateMachineModelFactory
which is able to use base
repository interfaces StateRepository
, TransitionRepository
,
ActionRepository
and GuardRepository
accompanied with base entity
interfaces RepositoryState
, RepositoryTransition
,
RepositoryAction
and RepositoryGuard
respectively.
Due to way how Entities and Repositories work in a Spring Data,
from a user perspective read access can be fully abstracted as it is
done in RepositoryStateMachineModelFactory
as there is no need to
know what is a real mapped Entity class Repository is working
with. Writing into a Repository is always dependant of using a real
Repository specific Entity class. From machine configuration point
of view we don’t need to know these, meaning we don’t need to know
actual implementation whether that is JPA, Redis or anything else
what Spring Data supports. Using a real Repository related
Entity class comes into play when you manually try to write new
states or transitions into a backed repository.
Tip | |
---|---|
Entity classes for RepositoryState and RepositoryTransition have
|
Actual out of a box implementations are documented in below sections where images below are uml equivalent statecharts of a repository configs.
Actual Repository implementations for a JPA are
JpaStateRepository
, JpaTransitionRepository
, JpaActionRepository
and JpaGuardRepository
which are backed by
Entity classes JpaRepositoryState
, JpaRepositoryTransition
,
JpaRepositoryAction
and JpaRepositoryGuard
respectively.
Important | |
---|---|
Version '1.2.8' unfortunately had to made a change into JPA’s Entity model regarding used table names. Previously generated table names always had a prefix 'JPA_REPOSITORY_' derived from Entity class names. As this caused breaking issues with databases imposing restrictions on database object lengths, all Entity classes have spesific definitions to force table names. For example 'JPA_REPOSITORY_STATE' is now simple 'STATE' and so on with other Entity classes. |
Generic way to update states and transition manually for jpa is shown below. This is equivalent to machine shown in Figure 34.1, “SimpleMachine”.
@Autowired StateRepository<JpaRepositoryState> stateRepository; @Autowired TransitionRepository<JpaRepositoryTransition> transitionRepository; void addConfig() { JpaRepositoryState stateS1 = new JpaRepositoryState("S1", true); JpaRepositoryState stateS2 = new JpaRepositoryState("S2"); JpaRepositoryState stateS3 = new JpaRepositoryState("S3"); stateRepository.save(stateS1); stateRepository.save(stateS2); stateRepository.save(stateS3); JpaRepositoryTransition transitionS1ToS2 = new JpaRepositoryTransition(stateS1, stateS2, "E1"); JpaRepositoryTransition transitionS2ToS3 = new JpaRepositoryTransition(stateS2, stateS3, "E2"); transitionRepository.save(transitionS1ToS2); transitionRepository.save(transitionS2ToS3); }
This is equivalent to machine shown in Figure 34.2, “SimpleSubMachine”.
@Autowired StateRepository<JpaRepositoryState> stateRepository; @Autowired TransitionRepository<JpaRepositoryTransition> transitionRepository; void addConfig() { JpaRepositoryState stateS1 = new JpaRepositoryState("S1", true); JpaRepositoryState stateS2 = new JpaRepositoryState("S2"); JpaRepositoryState stateS3 = new JpaRepositoryState("S3"); JpaRepositoryState stateS21 = new JpaRepositoryState("S21", true); stateS21.setParentState(stateS2); JpaRepositoryState stateS22 = new JpaRepositoryState("S22"); stateS22.setParentState(stateS2); stateRepository.save(stateS1); stateRepository.save(stateS2); stateRepository.save(stateS3); stateRepository.save(stateS21); stateRepository.save(stateS22); JpaRepositoryTransition transitionS1ToS2 = new JpaRepositoryTransition(stateS1, stateS2, "E1"); JpaRepositoryTransition transitionS2ToS3 = new JpaRepositoryTransition(stateS21, stateS22, "E2"); JpaRepositoryTransition transitionS21ToS22 = new JpaRepositoryTransition(stateS2, stateS3, "E3"); transitionRepository.save(transitionS1ToS2); transitionRepository.save(transitionS2ToS3); transitionRepository.save(transitionS21ToS22); }
This is equivalent to machine shown in Figure 34.3, “ShowcaseMachine”.
First you access all repositories.
@Autowired StateRepository<JpaRepositoryState> stateRepository; @Autowired TransitionRepository<JpaRepositoryTransition> transitionRepository; @Autowired ActionRepository<JpaRepositoryAction> actionRepository; @Autowired GuardRepository<JpaRepositoryGuard> guardRepository;
Create actions and guards.
JpaRepositoryGuard foo0Guard = new JpaRepositoryGuard(); foo0Guard.setName("foo0Guard"); JpaRepositoryGuard foo1Guard = new JpaRepositoryGuard(); foo1Guard.setName("foo1Guard"); JpaRepositoryAction fooAction = new JpaRepositoryAction(); fooAction.setName("fooAction"); guardRepository.save(foo0Guard); guardRepository.save(foo1Guard); actionRepository.save(fooAction);
Create states.
JpaRepositoryState stateS0 = new JpaRepositoryState("S0", true); stateS0.setInitialAction(fooAction); JpaRepositoryState stateS1 = new JpaRepositoryState("S1", true); stateS1.setParentState(stateS0); JpaRepositoryState stateS11 = new JpaRepositoryState("S11", true); stateS11.setParentState(stateS1); JpaRepositoryState stateS12 = new JpaRepositoryState("S12"); stateS12.setParentState(stateS1); JpaRepositoryState stateS2 = new JpaRepositoryState("S2"); stateS2.setParentState(stateS0); JpaRepositoryState stateS21 = new JpaRepositoryState("S21", true); stateS21.setParentState(stateS2); JpaRepositoryState stateS211 = new JpaRepositoryState("S211", true); stateS211.setParentState(stateS21); JpaRepositoryState stateS212 = new JpaRepositoryState("S212"); stateS212.setParentState(stateS21); stateRepository.save(stateS0); stateRepository.save(stateS1); stateRepository.save(stateS11); stateRepository.save(stateS12); stateRepository.save(stateS2); stateRepository.save(stateS21); stateRepository.save(stateS211); stateRepository.save(stateS212);
Finally create transitions.
JpaRepositoryTransition transitionS1ToS1 = new JpaRepositoryTransition(stateS1, stateS1, "A"); transitionS1ToS1.setGuard(foo1Guard); JpaRepositoryTransition transitionS1ToS11 = new JpaRepositoryTransition(stateS1, stateS11, "B"); JpaRepositoryTransition transitionS21ToS211 = new JpaRepositoryTransition(stateS21, stateS211, "B"); JpaRepositoryTransition transitionS1ToS2 = new JpaRepositoryTransition(stateS1, stateS2, "C"); JpaRepositoryTransition transitionS1ToS0 = new JpaRepositoryTransition(stateS1, stateS0, "D"); JpaRepositoryTransition transitionS211ToS21 = new JpaRepositoryTransition(stateS211, stateS21, "D"); JpaRepositoryTransition transitionS0ToS211 = new JpaRepositoryTransition(stateS0, stateS211, "E"); JpaRepositoryTransition transitionS1ToS211 = new JpaRepositoryTransition(stateS1, stateS211, "F"); JpaRepositoryTransition transitionS2ToS21 = new JpaRepositoryTransition(stateS2, stateS21, "F"); JpaRepositoryTransition transitionS11ToS211 = new JpaRepositoryTransition(stateS11, stateS211, "G"); JpaRepositoryTransition transitionS0 = new JpaRepositoryTransition(stateS0, stateS0, "H"); transitionS0.setKind(TransitionKind.INTERNAL); transitionS0.setGuard(foo0Guard); transitionS0.setActions(new HashSet<>(Arrays.asList(fooAction))); JpaRepositoryTransition transitionS1 = new JpaRepositoryTransition(stateS1, stateS1, "H"); transitionS1.setKind(TransitionKind.INTERNAL); JpaRepositoryTransition transitionS2 = new JpaRepositoryTransition(stateS2, stateS2, "H"); transitionS2.setKind(TransitionKind.INTERNAL); transitionS2.setGuard(foo1Guard); transitionS2.setActions(new HashSet<>(Arrays.asList(fooAction))); JpaRepositoryTransition transitionS11ToS12 = new JpaRepositoryTransition(stateS11, stateS12, "I"); JpaRepositoryTransition transitionS12ToS212 = new JpaRepositoryTransition(stateS12, stateS212, "I"); JpaRepositoryTransition transitionS211ToS12 = new JpaRepositoryTransition(stateS211, stateS12, "I"); JpaRepositoryTransition transitionS11 = new JpaRepositoryTransition(stateS11, stateS11, "J"); JpaRepositoryTransition transitionS2ToS1 = new JpaRepositoryTransition(stateS2, stateS1, "K"); transitionRepository.save(transitionS1ToS1); transitionRepository.save(transitionS1ToS11); transitionRepository.save(transitionS21ToS211); transitionRepository.save(transitionS1ToS2); transitionRepository.save(transitionS1ToS0); transitionRepository.save(transitionS211ToS21); transitionRepository.save(transitionS0ToS211); transitionRepository.save(transitionS1ToS211); transitionRepository.save(transitionS2ToS21); transitionRepository.save(transitionS11ToS211); transitionRepository.save(transitionS0); transitionRepository.save(transitionS1); transitionRepository.save(transitionS2); transitionRepository.save(transitionS11ToS12); transitionRepository.save(transitionS12ToS212); transitionRepository.save(transitionS211ToS12); transitionRepository.save(transitionS11); transitionRepository.save(transitionS2ToS1);
Complete example can be found from sample Chapter 50, JPA Config. This example is also showing how repository can be pre-populated from existing json file having a definitions for entity classes.
Actual Repository implementations for a Redis are
RedisStateRepository
, RedisTransitionRepository
, RedisActionRepository
and RedisGuardRepository
which are backed by
Entity classes RedisRepositoryState
, RedisRepositoryTransition
,
RedisRepositoryAction
and RedisRepositoryGuard
respectively.
Generic way to update states and transition manually for redis is shown below. This is equivalent to machine shown in Figure 34.1, “SimpleMachine”.
@Autowired StateRepository<RedisRepositoryState> stateRepository; @Autowired TransitionRepository<RedisRepositoryTransition> transitionRepository; void addConfig() { RedisRepositoryState stateS1 = new RedisRepositoryState("S1", true); RedisRepositoryState stateS2 = new RedisRepositoryState("S2"); RedisRepositoryState stateS3 = new RedisRepositoryState("S3"); stateRepository.save(stateS1); stateRepository.save(stateS2); stateRepository.save(stateS3); RedisRepositoryTransition transitionS1ToS2 = new RedisRepositoryTransition(stateS1, stateS2, "E1"); RedisRepositoryTransition transitionS2ToS3 = new RedisRepositoryTransition(stateS2, stateS3, "E2"); transitionRepository.save(transitionS1ToS2); transitionRepository.save(transitionS2ToS3); }
This is equivalent to machine shown in Figure 34.2, “SimpleSubMachine”.
@Autowired StateRepository<RedisRepositoryState> stateRepository; @Autowired TransitionRepository<RedisRepositoryTransition> transitionRepository; void addConfig() { RedisRepositoryState stateS1 = new RedisRepositoryState("S1", true); RedisRepositoryState stateS2 = new RedisRepositoryState("S2"); RedisRepositoryState stateS3 = new RedisRepositoryState("S3"); stateRepository.save(stateS1); stateRepository.save(stateS2); stateRepository.save(stateS3); RedisRepositoryTransition transitionS1ToS2 = new RedisRepositoryTransition(stateS1, stateS2, "E1"); RedisRepositoryTransition transitionS2ToS3 = new RedisRepositoryTransition(stateS2, stateS3, "E2"); transitionRepository.save(transitionS1ToS2); transitionRepository.save(transitionS2ToS3); }
Actual Repository implementations for a MongoDB are
MongoDbStateRepository
, MongoDbTransitionRepository
, MongoDbActionRepository
and MongoDbGuardRepository
which are backed by
Entity classes MongoDbRepositoryState
, MongoDbRepositoryTransition
,
MongoDbRepositoryAction
and MongoDbRepositoryGuard
respectively.
Generic way to update states and transition manually for redis is shown below. This is equivalent to machine shown in Figure 34.1, “SimpleMachine”.
@Autowired StateRepository<MongoDbRepositoryState> stateRepository; @Autowired TransitionRepository<MongoDbRepositoryTransition> transitionRepository; void addConfig() { MongoDbRepositoryState stateS1 = new MongoDbRepositoryState("S1", true); MongoDbRepositoryState stateS2 = new MongoDbRepositoryState("S2"); MongoDbRepositoryState stateS3 = new MongoDbRepositoryState("S3"); stateRepository.save(stateS1); stateRepository.save(stateS2); stateRepository.save(stateS3); MongoDbRepositoryTransition transitionS1ToS2 = new MongoDbRepositoryTransition(stateS1, stateS2, "E1"); MongoDbRepositoryTransition transitionS2ToS3 = new MongoDbRepositoryTransition(stateS2, stateS3, "E2"); transitionRepository.save(transitionS1ToS2); transitionRepository.save(transitionS2ToS3); }
This is equivalent to machine shown in Figure 34.2, “SimpleSubMachine”.
@Autowired StateRepository<MongoDbRepositoryState> stateRepository; @Autowired TransitionRepository<MongoDbRepositoryTransition> transitionRepository; void addConfig() { MongoDbRepositoryState stateS1 = new MongoDbRepositoryState("S1", true); MongoDbRepositoryState stateS2 = new MongoDbRepositoryState("S2"); MongoDbRepositoryState stateS3 = new MongoDbRepositoryState("S3"); MongoDbRepositoryState stateS21 = new MongoDbRepositoryState("S21", true); stateS21.setParentState(stateS2); MongoDbRepositoryState stateS22 = new MongoDbRepositoryState("S22"); stateS22.setParentState(stateS2); stateRepository.save(stateS1); stateRepository.save(stateS2); stateRepository.save(stateS3); stateRepository.save(stateS21); stateRepository.save(stateS22); MongoDbRepositoryTransition transitionS1ToS2 = new MongoDbRepositoryTransition(stateS1, stateS2, "E1"); MongoDbRepositoryTransition transitionS2ToS3 = new MongoDbRepositoryTransition(stateS21, stateS22, "E2"); MongoDbRepositoryTransition transitionS21ToS22 = new MongoDbRepositoryTransition(stateS2, stateS3, "E3"); transitionRepository.save(transitionS1ToS2); transitionRepository.save(transitionS2ToS3); transitionRepository.save(transitionS21ToS22); }
Apart from storing machine configuration, shown in Section 34.1, “Repository Config”, in an external repository it is also possible to persist machine into repositories.
Interface StateMachineRepository
is a central access point
interacting with machine persistence and is backed by Entity class
RepositoryStateMachine
.
Actual Repository implementation for a JPA is
JpaStateMachineRepository
which is backed by Entity class
JpaRepositoryStateMachine
.
Generic way to persist machine for jpa is shown below.
@Autowired StateMachineRepository<JpaRepositoryStateMachine> stateMachineRepository; void persist() { JpaRepositoryStateMachine machine = new JpaRepositoryStateMachine(); machine.setMachineId("machine"); machine.setState("S1"); // raw byte[] representation of a context machine.setStateMachineContext(new byte[] { 0 }); stateMachineRepository.save(machine); }
Actual Repository implementation for a Redis is
RedisStateMachineRepository
which is backed by Entity class
RedisRepositoryStateMachine
.
Generic way to persist machine for jpa is shown below.
@Autowired StateMachineRepository<RedisRepositoryStateMachine> stateMachineRepository; void persist() { RedisRepositoryStateMachine machine = new RedisRepositoryStateMachine(); machine.setMachineId("machine"); machine.setState("S1"); // raw byte[] representation of a context machine.setStateMachineContext(new byte[] { 0 }); stateMachineRepository.save(machine); }
Actual Repository implementation for a MongoDB is
MongoDbStateMachineRepository
which is backed by Entity class
MongoDbRepositoryStateMachine
.
Generic way to persist machine for jpa is shown below.
@Autowired StateMachineRepository<MongoDbRepositoryStateMachine> stateMachineRepository; void persist() { MongoDbRepositoryStateMachine machine = new MongoDbRepositoryStateMachine(); machine.setMachineId("machine"); machine.setState("S1"); // raw byte[] representation of a context machine.setStateMachineContext(new byte[] { 0 }); stateMachineRepository.save(machine); }
This chapter contains documentation for existing built-in state machine recipes.
What exactly is a recipe? As Spring Statemachine is always going to be a foundational framework meaning that its core will not have that much higher level functionality or dependencies outside of a Spring Framework. Correct usage of a state machine may be a little difficult time to time and there’s always some common use cases how state machine can be used. Recipe modules are meant to provide a higher level solutions to these common use cases and also provide examples beyond samples how framework can be used.
Note | |
---|---|
Recipes are a great way to make external contributions this Spring Statemachine project. If you’re not ready to contribute to the framework core itself, a custom and common recipe is a great way to share functionality among other users. |
Persist recipe is a simple utility which allows to use a single state machine instance to persist and update a state of an arbitrary item in a repository.
Recipes main class is PersistStateMachineHandler
which assumes user
to do three different things:
StateMachine<String, String>
needs to be used
with a PersistStateMachineHandler
. States and Events are required
to be type of Strings.
PersistStateChangeListener
need to be registered with handler
order to react to persist request.
handleEventWithState
is used to orchestrate state changes.
There is a sample demonstrating usage of this recipe at Chapter 42, Persist.
Tasks recipe is a concept to execute DAG of Runnable
instances using
a state machine. This recipe has been developed from ideas introduced
in sample Chapter 40, Tasks.
Generic concept of a state machine is shown below. In this state chart
everything under TASKS
just shows a generic concept of how a single
task is executed. Because this recipe allows to register deep
hierarchical DAG of tasks, meaning a real state chart would be deep
nested collection of sub-states and regions, there’s no need to be
more precise.
For example if you have only two registered tasks, below state chart
would be correct with TASK_id
replaced with TASK_1
and TASK_2
if
registered tasks ids are 1
and 2
.
Executing a Runnable
may result an error and especially if a complex
DAG of tasks is involved it is desirable that there is a way to handle
tasks execution errors and then having a way to continue execution
without executing already successfully executed tasks. Addition to
this it would be nice if some execution errors can be handled
automatically and as a last fallback, if error can’t be handled
automatically, state machine is put into a state where user can handle
errors manually.
TasksHandler
contains a builder method to configure handler instance
and follows a simple builder pattern. This builder can be used to
register Runnable
tasks, TasksListener
instances, define
StateMachinePersist
hook, and setup custom TaskExecutor
instance.
Now let’s take a simple Runnable
just doing a simple sleep as shown
below. This is a base of all examples in this chapter.
private Runnable sleepRunnable() { return new Runnable() { @Override public void run() { try { Thread.sleep(2000); } catch (InterruptedException e) { } } }; }
To execute multiple sleepRunnable
tasks just register tasks and
execute runTasks()
method from TasksHandler
.
TasksHandler handler = TasksHandler.builder() .task("1", sleepRunnable()) .task("2", sleepRunnable()) .task("3", sleepRunnable()) .build(); handler.runTasks();
Order to listen what is happening with a task execution an instance of
a TasksListener
can be registered with a TasksHandler
. Recipe
provides an adapter TasksListenerAdapter
if you don’t want to
implement a full interface. Listener provides a various hooks to
listen tasks execution events.
private class MyTasksListener extends TasksListenerAdapter { @Override public void onTasksStarted() { } @Override public void onTasksContinue() { } @Override public void onTaskPreExecute(Object id) { } @Override public void onTaskPostExecute(Object id) { } @Override public void onTaskFailed(Object id, Exception exception) { } @Override public void onTaskSuccess(Object id) { } @Override public void onTasksSuccess() { } @Override public void onTasksError() { } @Override public void onTasksAutomaticFix(TasksHandler handler, StateContext<String, String> context) { } }
Listeners can be either registered via a builder or directly with a
TasksHandler
as shown above.
MyTasksListener listener1 = new MyTasksListener(); MyTasksListener listener2 = new MyTasksListener(); TasksHandler handler = TasksHandler.builder() .task("1", sleepRunnable()) .task("2", sleepRunnable()) .task("3", sleepRunnable()) .listener(listener1) .build(); handler.addTasksListener(listener2); handler.removeTasksListener(listener2); handler.runTasks();
Above sample show how to create a deep nested DAG of tasks. Every task needs to have an unique identifier and optionally as task can be defined to be a sub-task. Effectively this will create a DAG of tasks.
TasksHandler handler = TasksHandler.builder() .task("1", sleepRunnable()) .task("1", "12", sleepRunnable()) .task("1", "13", sleepRunnable()) .task("2", sleepRunnable()) .task("2", "22", sleepRunnable()) .task("2", "23", sleepRunnable()) .task("3", sleepRunnable()) .task("3", "32", sleepRunnable()) .task("3", "33", sleepRunnable()) .build(); handler.runTasks();
When error happens and a state machine running these tasks goes into a
ERROR
state, user can call handler methods fixCurrentProblems
to
reset current state of tasks kept in a state machine extended state
variables. Handler method continueFromError
can then be used to
instruct state machine to transition from ERROR
state back to
READY
state where tasks can be executed again.
TasksHandler handler = TasksHandler.builder() .task("1", sleepRunnable()) .task("2", sleepRunnable()) .task("3", sleepRunnable()) .build(); handler.runTasks(); handler.fixCurrentProblems(); handler.continueFromError();
This part of the reference documentation explains the use of state machines together with a sample code and a uml state charts. We do few shortcuts when representing relationship between a state chart, SSM configuration and what an application does with a state machine. For complete examples go and study the samples repository.
Samples are build directly from a main source distribution during a normal build cycle. Samples in this chapter are:
Chapter 37, Turnstile Turnstile.
Chapter 38, Showcase Showcase.
Chapter 39, CD Player CD Player.
Chapter 40, Tasks Tasks.
Chapter 41, Washer Washer.
Chapter 42, Persist Persist.
Chapter 43, Zookeeper Zookeeper.
Chapter 44, Web Web.
Chapter 45, Scope Scope.
Chapter 46, Security Security.
Chapter 47, Event Service Event Service.
Chapter 48, Deploy Deploy.
Chapter 49, Order Shipping Order Shipping.
Chapter 50, JPA Config JPA Config.
Chapter 51, Data Persist Data Persist.
Chapter 52, Data Multi Persist Data Multi Persist.
Chapter 53, Monitoring Monitoring.
./gradlew clean build -x test
Every sample is located in its own directory under
spring-statemachine-samples
. Samples are based on spring-boot and
spring-shell and you will find usual boot fat jars under every sample
projects build/libs
directory.
Note | |
---|---|
Filenames for jars we refer in this section are populated during a
build of this document, meaning if you’re building samples from a
master, you have files with |
Turnstile is a simple device which gives you an access if payment is
made and is a very simple to model using a state machine. In its
simplest form there are only two states, LOCKED
and UNLOCKED
. Two
events, COIN
and PUSH
can happen if you try to go through it or
you make a payment.
States.
public enum States {
LOCKED, UNLOCKED
}
Events.
public enum Events {
COIN, PUSH
}
Configuration.
@Configuration @EnableStateMachine static class StateMachineConfig extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.LOCKED) .states(EnumSet.allOf(States.class)); } @Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.LOCKED) .target(States.UNLOCKED) .event(Events.COIN) .and() .withExternal() .source(States.UNLOCKED) .target(States.LOCKED) .event(Events.PUSH); } }
You can see how this sample state machine interacts with event by
running turnstile
sample.
$ java -jar spring-statemachine-samples-turnstile-2.1.1.RELEASE.jar sm>sm print +----------------------------------------------------------------+ | SM | +----------------------------------------------------------------+ | | | +----------------+ +----------------+ | | *-->| LOCKED | | UNLOCKED | | | +----------------+ +----------------+ | | +---| entry/ | | entry/ |---+ | | | | exit/ | | exit/ | | | | | | | | | | | | PUSH| | |---COIN-->| | |COIN | | | | | | | | | | | | | | | | | | | | |<--PUSH---| | | | | +-->| | | |<--+ | | | | | | | | +----------------+ +----------------+ | | | +----------------------------------------------------------------+ sm>sm start State changed to LOCKED State machine started sm>sm event COIN State changed to UNLOCKED Event COIN send sm>sm event PUSH State changed to LOCKED Event PUSH send
Showcase is a complex state machine showing all possible transition topologies up to four levels of state nesting.
States.
public enum States { S0, S1, S11, S12, S2, S21, S211, S212 }
Events.
public enum Events {
A, B, C, D, E, F, G, H, I
}
Configuration - states.
@Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.S0, fooAction()) .state(States.S0) .and() .withStates() .parent(States.S0) .initial(States.S1) .state(States.S1) .and() .withStates() .parent(States.S1) .initial(States.S11) .state(States.S11) .state(States.S12) .and() .withStates() .parent(States.S0) .state(States.S2) .and() .withStates() .parent(States.S2) .initial(States.S21) .state(States.S21) .and() .withStates() .parent(States.S21) .initial(States.S211) .state(States.S211) .state(States.S212); }
Configuration - transitions.
@Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.S1).target(States.S1).event(Events.A) .guard(foo1Guard()) .and() .withExternal() .source(States.S1).target(States.S11).event(Events.B) .and() .withExternal() .source(States.S21).target(States.S211).event(Events.B) .and() .withExternal() .source(States.S1).target(States.S2).event(Events.C) .and() .withExternal() .source(States.S2).target(States.S1).event(Events.C) .and() .withExternal() .source(States.S1).target(States.S0).event(Events.D) .and() .withExternal() .source(States.S211).target(States.S21).event(Events.D) .and() .withExternal() .source(States.S0).target(States.S211).event(Events.E) .and() .withExternal() .source(States.S1).target(States.S211).event(Events.F) .and() .withExternal() .source(States.S2).target(States.S11).event(Events.F) .and() .withExternal() .source(States.S11).target(States.S211).event(Events.G) .and() .withExternal() .source(States.S211).target(States.S0).event(Events.G) .and() .withInternal() .source(States.S0).event(Events.H) .guard(foo0Guard()) .action(fooAction()) .and() .withInternal() .source(States.S2).event(Events.H) .guard(foo1Guard()) .action(fooAction()) .and() .withInternal() .source(States.S1).event(Events.H) .and() .withExternal() .source(States.S11).target(States.S12).event(Events.I) .and() .withExternal() .source(States.S211).target(States.S212).event(Events.I) .and() .withExternal() .source(States.S12).target(States.S212).event(Events.I); }
Configuration - actions and guard.
@Bean public FooGuard foo0Guard() { return new FooGuard(0); } @Bean public FooGuard foo1Guard() { return new FooGuard(1); } @Bean public FooAction fooAction() { return new FooAction(); }
Action.
private static class FooAction implements Action<States, Events> { @Override public void execute(StateContext<States, Events> context) { Map<Object, Object> variables = context.getExtendedState().getVariables(); Integer foo = context.getExtendedState().get("foo", Integer.class); if (foo == null) { log.info("Init foo to 0"); variables.put("foo", 0); } else if (foo == 0) { log.info("Switch foo to 1"); variables.put("foo", 1); } else if (foo == 1) { log.info("Switch foo to 0"); variables.put("foo", 0); } } }
Guard.
private static class FooGuard implements Guard<States, Events> { private final int match; public FooGuard(int match) { this.match = match; } @Override public boolean evaluate(StateContext<States, Events> context) { Object foo = context.getExtendedState().getVariables().get("foo"); return !(foo == null || !foo.equals(match)); } }
Let’s go through what this state machine do when it’s executed and we send various event to it.
sm>sm start Init foo to 0 Entry state S0 Entry state S1 Entry state S11 State machine started sm>sm event A Event A send sm>sm event C Exit state S11 Exit state S1 Entry state S2 Entry state S21 Entry state S211 Event C send sm>sm event H Switch foo to 1 Internal transition source=S0 Event H send sm>sm event C Exit state S211 Exit state S21 Exit state S2 Entry state S1 Entry state S11 Event C send sm>sm event A Exit state S11 Exit state S1 Entry state S1 Entry state S11 Event A send
What happens in above sample:
foo
is
init to 0
.
foo
to
be 1
.
foo
. Then we simply go back using event C.
Let’s take closer look of how hierarchical states and their event handling works with a below example.
sm>sm variables No variables sm>sm start Init foo to 0 Entry state S0 Entry state S1 Entry state S11 State machine started sm>sm variables foo=0 sm>sm event H Internal transition source=S1 Event H send sm>sm variables foo=0 sm>sm event C Exit state S11 Exit state S1 Entry state S2 Entry state S21 Entry state S211 Event C send sm>sm variables foo=0 sm>sm event H Switch foo to 1 Internal transition source=S0 Event H send sm>sm variables foo=1 sm>sm event H Switch foo to 0 Internal transition source=S2 Event H send sm>sm variables foo=0
What happens in above sample:
foo
flag
is always flipped around. However in state S1 event H always
match to its dummy transition without guard or action, not never
happens.
CD Player is a sample which resembles better use case of most of use have used in a real world. CD Player itself is a really simple entity where user can open a deck, insert or change a disk, then drive player functionality by pressing various buttons like eject, play, stop, pause, rewind and backward.
How many of us have really given a thought of what it will take to make a code for a CD Player which interacts with a hardware. Yes, concept of a player is overly simple but if you look behind the scenes things actually get a bit convoluted.
You’ve probably noticed that if your deck is open and you press play, deck will close and a song will start to play if CD was inserted in a first place. In a sense when deck is open you first need to close it and then try to start playing if cd is actually inserted. Hopefully you have now realised that a simple CD Player is not anymore so simple. Sure you can wrap all this with a simple class with few boolean variables and probably few nested if/else clauses, that will do the job, but what about if you need to make all this behaviour much more complex, do you really want to keep adding more flags and if/else clauses.
Let’s go through how this sample and its state machine is designed and how those two interacts with each other. Below three config sections are used withing a EnumStateMachineConfigurerAdapter.
@Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.IDLE) .state(States.IDLE) .and() .withStates() .parent(States.IDLE) .initial(States.CLOSED) .state(States.CLOSED, closedEntryAction(), null) .state(States.OPEN) .and() .withStates() .state(States.BUSY) .and() .withStates() .parent(States.BUSY) .initial(States.PLAYING) .state(States.PLAYING) .state(States.PAUSED); }
@Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.CLOSED).target(States.OPEN).event(Events.EJECT) .and() .withExternal() .source(States.OPEN).target(States.CLOSED).event(Events.EJECT) .and() .withExternal() .source(States.OPEN).target(States.CLOSED).event(Events.PLAY) .and() .withExternal() .source(States.PLAYING).target(States.PAUSED).event(Events.PAUSE) .and() .withInternal() .source(States.PLAYING) .action(playingAction()) .timer(1000) .and() .withInternal() .source(States.PLAYING).event(Events.BACK) .action(trackAction()) .and() .withInternal() .source(States.PLAYING).event(Events.FORWARD) .action(trackAction()) .and() .withExternal() .source(States.PAUSED).target(States.PLAYING).event(Events.PAUSE) .and() .withExternal() .source(States.BUSY).target(States.IDLE).event(Events.STOP) .and() .withExternal() .source(States.IDLE).target(States.BUSY).event(Events.PLAY) .action(playAction()) .guard(playGuard()) .and() .withInternal() .source(States.OPEN).event(Events.LOAD).action(loadAction()); }
@Bean public ClosedEntryAction closedEntryAction() { return new ClosedEntryAction(); } @Bean public LoadAction loadAction() { return new LoadAction(); } @Bean public TrackAction trackAction() { return new TrackAction(); } @Bean public PlayAction playAction() { return new PlayAction(); } @Bean public PlayingAction playingAction() { return new PlayingAction(); } @Bean public PlayGuard playGuard() { return new PlayGuard(); }
What we did in above configuration:
With transition we mostly mapped events to expected state transitions like EJECT closing and opening a deck, PLAY, STOP and PAUSE doing their natural transitions. Few words to mention what we did for other transitions.
This machine only have six states which are introduced as an enum.
public enum States { // super state of PLAYING and PAUSED BUSY, PLAYING, PAUSED, // super state of CLOSED and OPEN IDLE, CLOSED, OPEN }
Events represent, in a sense in this example, what buttons user would press and if user loads a cd disc into a deck.
public enum Events {
PLAY, STOP, PAUSE, EJECT, LOAD, FORWARD, BACK
}
Beans cdPlayer and library are just used with a sample to drive the application.
@Bean public CdPlayer cdPlayer() { return new CdPlayer(); } @Bean public Library library() { return Library.buildSampleLibrary(); }
We can define extended state variable key as simple enums.
public enum Variables { CD, TRACK, ELAPSEDTIME } public enum Headers { TRACKSHIFT }
We wanted to make this samply type safe so we’re defining our own annotation @StatesOnTransition which have a mandatory meta annotation @OnTransition.
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @OnTransition public @interface StatesOnTransition { States[] source() default {}; States[] target() default {}; }
ClosedEntryAction is a entry action for state CLOSED to simply send and PLAY event to a statemachine if cd disc is present.
public static class ClosedEntryAction implements Action<States, Events> { @Override public void execute(StateContext<States, Events> context) { if (context.getTransition() != null && context.getEvent() == Events.PLAY && context.getTransition().getTarget().getId() == States.CLOSED && context.getExtendedState().getVariables().get(Variables.CD) != null) { context.getStateMachine().sendEvent(Events.PLAY); } } }
LoadAction is simply updating extended state variable if event headers contained information about a cd disc to load.
public static class LoadAction implements Action<States, Events> { @Override public void execute(StateContext<States, Events> context) { Object cd = context.getMessageHeader(Variables.CD); context.getExtendedState().getVariables().put(Variables.CD, cd); } }
PlayAction is simply resetting player elapsed time which is kept as an extended state variable.
public static class PlayAction implements Action<States, Events> { @Override public void execute(StateContext<States, Events> context) { context.getExtendedState().getVariables().put(Variables.ELAPSEDTIME, 0l); context.getExtendedState().getVariables().put(Variables.TRACK, 0); } }
PlayGuard is used to guard transition from IDLE to BUSY with event PLAY if extended state variable CD doesn’t indicate that cd disc has been loaded.
public static class PlayGuard implements Guard<States, Events> { @Override public boolean evaluate(StateContext<States, Events> context) { ExtendedState extendedState = context.getExtendedState(); return extendedState.getVariables().get(Variables.CD) != null; } }
PlayingAction is updating extended state variable ELAPSEDTIME which cd player itself can read and update lcd status. Action also handles track shift if user is going back or forward in tracks.
public static class PlayingAction implements Action<States, Events> { @Override public void execute(StateContext<States, Events> context) { Map<Object, Object> variables = context.getExtendedState().getVariables(); Object elapsed = variables.get(Variables.ELAPSEDTIME); Object cd = variables.get(Variables.CD); Object track = variables.get(Variables.TRACK); if (elapsed instanceof Long) { long e = ((Long)elapsed) + 1000l; if (e > ((Cd) cd).getTracks()[((Integer) track)].getLength()*1000) { context.getStateMachine().sendEvent(MessageBuilder .withPayload(Events.FORWARD) .setHeader(Headers.TRACKSHIFT.toString(), 1).build()); } else { variables.put(Variables.ELAPSEDTIME, e); } } } }
TrackAction handles track shift action if user is going back or forward in tracks. If it is a last track of a cd, playing is stopped and STOP event sent to a state machine.
public static class TrackAction implements Action<States, Events> { @Override public void execute(StateContext<States, Events> context) { Map<Object, Object> variables = context.getExtendedState().getVariables(); Object trackshift = context.getMessageHeader(Headers.TRACKSHIFT.toString()); Object track = variables.get(Variables.TRACK); Object cd = variables.get(Variables.CD); if (trackshift instanceof Integer && track instanceof Integer && cd instanceof Cd) { int next = ((Integer)track) + ((Integer)trackshift); if (next >= 0 && ((Cd)cd).getTracks().length > next) { variables.put(Variables.ELAPSEDTIME, 0l); variables.put(Variables.TRACK, next); } else if (((Cd)cd).getTracks().length <= next) { context.getStateMachine().sendEvent(Events.STOP); } } } }
One other important aspect of a state machines is that they have their own responsibilities mostly around handling states and all application level logic should be kept outside. This means that application needs to have a ways to interact with a state machine and below sample is how cdplayer does it order to update lcd status. Also pay attention that we annotated CdPlayer with @WithStateMachine which instructs state machine to find methods from your pojo which are then called with various transitions.
@OnTransition(target = "BUSY") public void busy(ExtendedState extendedState) { Object cd = extendedState.getVariables().get(Variables.CD); if (cd != null) { cdStatus = ((Cd)cd).getName(); } }
In above example we use @OnTransition annotation to hook a callback when transition happens with a target state BUSY.
@StatesOnTransition(target = {States.CLOSED, States.IDLE}) public void closed(ExtendedState extendedState) { Object cd = extendedState.getVariables().get(Variables.CD); if (cd != null) { cdStatus = ((Cd)cd).getName(); } else { cdStatus = "No CD"; } trackStatus = ""; }
@OnTransition we used above can only be used with strings which are matched from enums. @StatesOnTransition is then something what user can create into his own application to get a type safe annotation where a real enums can be used.
Let’s see an example how this state machine actually works.
sm>sm start Entry state IDLE Entry state CLOSED State machine started sm>cd lcd No CD sm>cd library 0: Greatest Hits 0: Bohemian Rhapsody 05:56 1: Another One Bites the Dust 03:36 1: Greatest Hits II 0: A Kind of Magic 04:22 1: Under Pressure 04:08 sm>cd eject Exit state CLOSED Entry state OPEN sm>cd load 0 Loading cd Greatest Hits sm>cd play Exit state OPEN Entry state CLOSED Exit state CLOSED Exit state IDLE Entry state BUSY Entry state PLAYING sm>cd lcd Greatest Hits Bohemian Rhapsody 00:03 sm>cd forward sm>cd lcd Greatest Hits Another One Bites the Dust 00:04 sm>cd stop Exit state PLAYING Exit state BUSY Entry state IDLE Entry state CLOSED sm>cd lcd Greatest Hits
What happened in above run:
Tasks is a sample demonstrating a parallel task handling within a regions and additionally adds an error handling to either automatically or manually fixing task problems before continuing back to a state where tasks can be run again.
On a high level what happens in this state machine is:
States.
public enum States {
READY,
FORK, JOIN, CHOICE,
TASKS, T1, T1E, T2, T2E, T3, T3E,
ERROR, AUTOMATIC, MANUAL
}
Events.
public enum Events {
RUN, FALLBACK, CONTINUE, FIX;
}
Configuration - states.
@Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.READY) .fork(States.FORK) .state(States.TASKS) .join(States.JOIN) .choice(States.CHOICE) .state(States.ERROR) .and() .withStates() .parent(States.TASKS) .initial(States.T1) .end(States.T1E) .and() .withStates() .parent(States.TASKS) .initial(States.T2) .end(States.T2E) .and() .withStates() .parent(States.TASKS) .initial(States.T3) .end(States.T3E) .and() .withStates() .parent(States.ERROR) .initial(States.AUTOMATIC) .state(States.AUTOMATIC, automaticAction(), null) .state(States.MANUAL); }
Configuration - transitions.
@Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.READY).target(States.FORK) .event(Events.RUN) .and() .withFork() .source(States.FORK).target(States.TASKS) .and() .withExternal() .source(States.T1).target(States.T1E) .and() .withExternal() .source(States.T2).target(States.T2E) .and() .withExternal() .source(States.T3).target(States.T3E) .and() .withJoin() .source(States.TASKS).target(States.JOIN) .and() .withExternal() .source(States.JOIN).target(States.CHOICE) .and() .withChoice() .source(States.CHOICE) .first(States.ERROR, tasksChoiceGuard()) .last(States.READY) .and() .withExternal() .source(States.ERROR).target(States.READY) .event(Events.CONTINUE) .and() .withExternal() .source(States.AUTOMATIC).target(States.MANUAL) .event(Events.FALLBACK) .and() .withInternal() .source(States.MANUAL) .action(fixAction()) .event(Events.FIX); }
Guard below is guarding choice entry into a ERROR state and needs to return TRUE if error has happened. For this guard simply checks that all extended state variables(T1, T2 and T3) are TRUE.
@Bean public Guard<States, Events> tasksChoiceGuard() { return new Guard<States, Events>() { @Override public boolean evaluate(StateContext<States, Events> context) { Map<Object, Object> variables = context.getExtendedState().getVariables(); return !(ObjectUtils.nullSafeEquals(variables.get("T1"), true) && ObjectUtils.nullSafeEquals(variables.get("T2"), true) && ObjectUtils.nullSafeEquals(variables.get("T3"), true)); } }; }
Actions below will simply send event to a state machine to request next step which would be either fallback or continue back to ready.
@Bean public Action<States, Events> automaticAction() { return new Action<States, Events>() { @Override public void execute(StateContext<States, Events> context) { Map<Object, Object> variables = context.getExtendedState().getVariables(); if (ObjectUtils.nullSafeEquals(variables.get("T1"), true) && ObjectUtils.nullSafeEquals(variables.get("T2"), true) && ObjectUtils.nullSafeEquals(variables.get("T3"), true)) { context.getStateMachine().sendEvent(Events.CONTINUE); } else { context.getStateMachine().sendEvent(Events.FALLBACK); } } }; } @Bean public Action<States, Events> fixAction() { return new Action<States, Events>() { @Override public void execute(StateContext<States, Events> context) { Map<Object, Object> variables = context.getExtendedState().getVariables(); variables.put("T1", true); variables.put("T2", true); variables.put("T3", true); context.getStateMachine().sendEvent(Events.CONTINUE); } }; }
Currently default region execution is synchronous but it can be
changed to asynchronous by changing TaskExecutor
. Task will simulate
work by sleeping 2 seconds so you’ll able to see how actions in
regions are executed parallel.
@Bean(name = StateMachineSystemConstants.TASK_EXECUTOR_BEAN_NAME) public TaskExecutor taskExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(5); return taskExecutor; }
Let’s see an examples how this state machine actually works.
sm>sm start State machine started Entry state READY sm>tasks run Entry state TASKS run task on T3 run task on T2 run task on T1 run task on T2 done run task on T1 done run task on T3 done Entry state T2 Entry state T3 Entry state T1 Entry state T1E Entry state T2E Entry state T3E Exit state TASKS Entry state JOIN Exit state JOIN Entry state READY
In above we can execute tasks multiple times.
sm>tasks list Tasks {T1=true, T3=true, T2=true} sm>tasks fail T1 sm>tasks list Tasks {T1=false, T3=true, T2=true} sm>tasks run Entry state TASKS run task on T1 run task on T3 run task on T2 run task on T1 done run task on T3 done run task on T2 done Entry state T1 Entry state T3 Entry state T2 Entry state T1E Entry state T2E Entry state T3E Exit state TASKS Entry state JOIN Exit state JOIN Entry state ERROR Entry state AUTOMATIC Exit state AUTOMATIC Exit state ERROR Entry state READY
In above, if we simulate failure for task T1, it is fixed automatically.
sm>tasks list Tasks {T1=true, T3=true, T2=true} sm>tasks fail T2 sm>tasks run Entry state TASKS run task on T2 run task on T1 run task on T3 run task on T2 done run task on T1 done run task on T3 done Entry state T2 Entry state T1 Entry state T3 Entry state T1E Entry state T2E Entry state T3E Exit state TASKS Entry state JOIN Exit state JOIN Entry state ERROR Entry state AUTOMATIC Exit state AUTOMATIC Entry state MANUAL sm>tasks fix Exit state MANUAL Exit state ERROR Entry state READY
In above if we simulate failure for either task T2 or T3, state machine goes to MANUAL state where problem needs to be fixed manually before we’re able to go back to READY state.
Washer is a sample demonstrating a use of a history state to recover a running state configuration with a simulated power off situation.
Anyone ever used a washing machine knows that if you can somehow pause the program it will continue from a same state when lid is closed. This kind of behaviour can be implemented in a state machine by using a history pseudo state.
States.
public enum States {
RUNNING, HISTORY, END,
WASHING, RINSING, DRYING,
POWEROFF
}
Events.
public enum Events {
RINSE, DRY, STOP,
RESTOREPOWER, CUTPOWER
}
Configuration - states.
@Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.RUNNING) .state(States.POWEROFF) .end(States.END) .and() .withStates() .parent(States.RUNNING) .initial(States.WASHING) .state(States.RINSING) .state(States.DRYING) .history(States.HISTORY, History.SHALLOW); }
Configuration - transitions.
@Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.WASHING).target(States.RINSING) .event(Events.RINSE) .and() .withExternal() .source(States.RINSING).target(States.DRYING) .event(Events.DRY) .and() .withExternal() .source(States.RUNNING).target(States.POWEROFF) .event(Events.CUTPOWER) .and() .withExternal() .source(States.POWEROFF).target(States.HISTORY) .event(Events.RESTOREPOWER) .and() .withExternal() .source(States.RUNNING).target(States.END) .event(Events.STOP); }
Let’s see an example how this state machine actually works.
sm>sm start Entry state RUNNING Entry state WASHING State machine started sm>sm event RINSE Exit state WASHING Entry state RINSING Event RINSE send sm>sm event DRY Exit state RINSING Entry state DRYING Event DRY send sm>sm event CUTPOWER Exit state DRYING Exit state RUNNING Entry state POWEROFF Event CUTPOWER send sm>sm event RESTOREPOWER Exit state POWEROFF Entry state RUNNING Entry state WASHING Entry state DRYING Event RESTOREPOWER send
What happened in above run:
Persist is a sample using recipe Chapter 35, Persist to demonstrate how a database entry update logic can be controlled by a state machine.
The state machine logic and configuration is shown above:
StateMachine Config.
@Configuration @EnableStateMachine static class StateMachineConfig extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineStateConfigurer<String, String> states) throws Exception { states .withStates() .initial("PLACED") .state("PROCESSING") .state("SENT") .state("DELIVERED"); } @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions .withExternal() .source("PLACED").target("PROCESSING") .event("PROCESS") .and() .withExternal() .source("PROCESSING").target("SENT") .event("SEND") .and() .withExternal() .source("SENT").target("DELIVERED") .event("DELIVER"); } }
PersistStateMachineHandler
can be created using a below config:
Handler Config.
@Configuration static class PersistHandlerConfig { @Autowired private StateMachine<String, String> stateMachine; @Bean public Persist persist() { return new Persist(persistStateMachineHandler()); } @Bean public PersistStateMachineHandler persistStateMachineHandler() { return new PersistStateMachineHandler(stateMachine); } }
Order class used with this sample is shown below:
Order Class.
public static class Order { int id; String state; public Order(int id, String state) { this.id = id; this.state = state; } @Override public String toString() { return "Order [id=" + id + ", state=" + state + "]"; } }
Now let’s see how this example works.
sm>persist db Order [id=1, state=PLACED] Order [id=2, state=PROCESSING] Order [id=3, state=SENT] Order [id=4, state=DELIVERED] sm>persist process 1 Exit state PLACED Entry state PROCESSING sm>persist db Order [id=2, state=PROCESSING] Order [id=3, state=SENT] Order [id=4, state=DELIVERED] Order [id=1, state=PROCESSING] sm>persist deliver 3 Exit state SENT Entry state DELIVERED sm>persist db Order [id=2, state=PROCESSING] Order [id=4, state=DELIVERED] Order [id=1, state=PROCESSING] Order [id=3, state=DELIVERED]
What happened in above run:
1
into PROCESSING
state.
PLACED
into a PROCESSING
.
3
to update state from SENT
into
DELIVERED
.
Note | |
---|---|
If you’re wondering where is the database because there are literally no
signs of it in a sample code. Sample is based on Spring Boot and
because necessary classes are in a classpath, embedded Spring Boot will even create an instance of @Autowired private JdbcTemplate jdbcTemplate; |
Finally we need to handle state changes:
public void change(int order, String event) { Order o = jdbcTemplate.queryForObject("select id, state from orders where id = ?", new Object[] { order }, new RowMapper<Order>() { public Order mapRow(ResultSet rs, int rowNum) throws SQLException { return new Order(rs.getInt("id"), rs.getString("state")); } }); handler.handleEventWithState(MessageBuilder.withPayload(event).setHeader("order", order).build(), o.state); }
And use a PersistStateChangeListener
to update database:
private class LocalPersistStateChangeListener implements PersistStateChangeListener { @Override public void onPersist(State<String, String> state, Message<String> message, Transition<String, String> transition, StateMachine<String, String> stateMachine) { if (message != null && message.getHeaders().containsKey("order")) { Integer order = message.getHeaders().get("order", Integer.class); jdbcTemplate.update("update orders set state = ? where id = ?", state.getId(), order); } } }
Zookeeper is a distributed version from sample Chapter 37, Turnstile.
Note | |
---|---|
This sample needs and external |
Configuration of this sample is almost same as turnstile
sample. We
only add configuration for distributed state machine where we
configure StateMachineEnsemble
.
@Override public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception { config .withDistributed() .ensemble(stateMachineEnsemble()); }
Actual StateMachineEnsemble
needs to be created as bean together
with CuratorFramework
client.
@Bean public StateMachineEnsemble<String, String> stateMachineEnsemble() throws Exception { return new ZookeeperStateMachineEnsemble<String, String>(curatorClient(), "/foo"); } @Bean public CuratorFramework curatorClient() throws Exception { CuratorFramework client = CuratorFrameworkFactory.builder().defaultData(new byte[0]) .retryPolicy(new ExponentialBackoffRetry(1000, 3)) .connectString("localhost:2181").build(); client.start(); return client; }
Let’s go through a simple example where two different shell instances are started with command
@n1:~# java -jar spring-statemachine-samples-zookeeper-2.1.1.RELEASE.jar
First open first shell instance(do not start second instance yet).
When state machine is started it will end up into its initial state
LOCKED
. Then send event COIN
to transit into UNLOCKED
state.
Shell1.
sm>sm start Entry state LOCKED State machine started sm>sm event COIN Exit state LOCKED Entry state UNLOCKED Event COIN send sm>sm state UNLOCKED
Open second shell instance and start a state machine. You should see
that distributed state UNLOCKED
is entered instead of default
initial state LOCKED
.
Shell2.
sm>sm start State machine started sm>sm state UNLOCKED
Then from either of a shells(we use second instance here) send event
PUSH
to transit from UNLOCKED
into LOCKED
state.
Shell2.
sm>sm event PUSH Exit state UNLOCKED Entry state LOCKED Event PUSH send
In other shell you should see state getting changed automatically based on distributed state kept in Zookeeper.
Shell1.
sm>Exit state UNLOCKED Entry state LOCKED
Web is a distributed state machine example using a zookeeper to handle distributed state. This example is meant to be run on a multiple browser sessions against a multiple different hosts.
This sample is using a modified state machine structure from a Chapter 38, Showcase to work with a distributed state machine. The state machine logic is shown above:
Note | |
---|---|
Due to nature of this sample an instance of a |
Let’s go through a simple example where three different sample instances are
started. If you are running different instances on a same host you need to
distinguish used port by adding --server.port=<myport>
to the command.
Otherwise default port for each host will be 8080
.
In this sample run we have three hosts, n1
, n2
and n3
which all
have a local zookeeper instance running and a state machine sample running
on a port 8080
.
@n1:~# java -jar spring-statemachine-samples-web-2.1.1.RELEASE.jar @n2:~# java -jar spring-statemachine-samples-web-2.1.1.RELEASE.jar @n3:~# java -jar spring-statemachine-samples-web-2.1.1.RELEASE.jar
When all instances are running you should see all showing similar
information via a browser where states are S0
, S1
and S11
,
and extended state variable foo=0
. Main state is S11
.
When you press button Event C
in any of a browser window,
distributed state is changed to S211
which is the target state
denoted by transition associated with an event C
.
Then let’s press button Event H
and what is supposed to happen is
that internal transition is executed on all state machines changing
extended state variable foo
from value 0
to 1
. This change is
first done on a state machine receiving the event and then propagated
to other state machines. You should only see variable foo
to change
from 0
to 1
.
Last we simply send an event Event K
which is supposed to take state
machine state back to state S11
and you should see this happening in
all browser sessions.
Scope is a state machine example using a session scope to provide individual instance for every user.
This is a simple state machine having states S0
, S1
and S2
.
Transitions between those are controlled via events A
, B
and C
as shown in a state chart.
@n1:~# java -jar spring-statemachine-samples-scope-2.1.1.RELEASE.jar
When instance is running you can open a browser and play with a state machine. If you open same page using a different browser, i.e one in Chrome and one in Firefox, you should get a new state machine instance per user session.
Security is a state machine example using most of a combinations of securing a state machine. It is securing sending events, transitions and actions.
@n1:~# java -jar spring-statemachine-samples-secure-2.1.1.RELEASE.jar
We secure event sending with a users having a role USER
. None of
a other users imposed by a Spring Security can’t send events into a
state machine.
@Override public void configure(StateMachineConfigurationConfigurer<States, Events> config) throws Exception { config .withConfiguration() .autoStartup(true) .and() .withSecurity() .enabled(true) .event("hasRole('USER')"); }
In this sample we define two users, user having a role USER
and
admin having both roles USER
and ADMIN
. Authentication for both
user for password is password
.
@EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true) static class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user") .password("password") .roles("USER") .and() .withUser("admin") .password("password") .roles("USER", "ADMIN"); } }
We define various transitions between states according to a statechart
seen above. Only a user with active ADMIN
role can execute
external transitions between S2
and S3
. Similarly ADMIN
can only
execute internal transition in a state S1
.
@Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.S0).target(States.S1).event(Events.A) .and() .withExternal() .source(States.S1).target(States.S2).event(Events.B) .and() .withExternal() .source(States.S2).target(States.S0).event(Events.C) .and() .withExternal() .source(States.S2).target(States.S3).event(Events.E) .secured("ROLE_ADMIN", ComparisonType.ANY) .and() .withExternal() .source(States.S3).target(States.S0).event(Events.C) .and() .withInternal() .source(States.S0).event(Events.D) .action(adminAction()) .and() .withInternal() .source(States.S1).event(Events.F) .action(transitionAction()) .secured("ROLE_ADMIN", ComparisonType.ANY); }
Action
adminAction
is secured with a role ADMIN
.
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) @Bean public Action<States, Events> adminAction() { return new Action<States, Events>() { @Secured("ROLE_ADMIN") @Override public void execute(StateContext<States, Events> context) { log.info("Executed only for admin role"); } }; }
Below Action
would only be executed with internal transition in a
state S1
when event F
is send. Transition itself is secured with a
role ADMIN
so this transition will not be executed if current user
does not hate that role.
@Bean public Action<States, Events> transitionAction() { return new Action<States, Events>() { @Override public void execute(StateContext<States, Events> context) { log.info("Executed only for admin role"); } }; }
Event Service is an example how state machine concepts can be used as a processing engine for events. Sample was born out from a question:
Note | |
---|---|
Can Spring Statemachine be used as a microservice to feed events to it with millions different state machine instances. |
In this example we will use a Redis
to persist a state machine
instances.
Obviously a million state machine instances in a jvm would be
a relatively bad idea due to memory constraints. This simply leads to
other available features from a Spring Statemachine to persist a
StateMachineContext
and re-use existing instances.
We assume few things like there is a shopping application which is
sending different types of PageView
events into a separate
microservice which is then tracking user behaviour using a state
machine. State model is shown below which simply have few states
representing user navigating on product items list, add and remove
items from a cart and going to a payment page and initiating a pay
operation. Actual shopping application would send these events into
this service for example using a simple rest calls. More about this
later.
Note | |
---|---|
Remember that focus here is to have an application which is exposing a
|
In below state machine configuration we simply model what we have in a
state chart. Various actions are updating state machine Extended
State
to track number of entries into various states and also how
many times internal transition for ADD
and DEL
are called and if
PAY
has been executed. Don’t focus on stateMachineTarget
or
@Scope
for now, as we’ll explain those in a bit.
@Bean(name = "stateMachineTarget") @Scope(scopeName="prototype") public StateMachine<States, Events> stateMachineTarget() throws Exception { Builder<States, Events> builder = StateMachineBuilder.<States, Events>builder(); builder.configureConfiguration() .withConfiguration() .autoStartup(true); builder.configureStates() .withStates() .initial(States.HOME) .states(EnumSet.allOf(States.class)); builder.configureTransitions() .withInternal() .source(States.ITEMS).event(Events.ADD) .action(addAction()) .and() .withInternal() .source(States.CART).event(Events.DEL) .action(delAction()) .and() .withInternal() .source(States.PAYMENT).event(Events.PAY) .action(payAction()) .and() .withExternal() .source(States.HOME).target(States.ITEMS) .action(pageviewAction()) .event(Events.VIEW_I) .and() .withExternal() .source(States.CART).target(States.ITEMS) .action(pageviewAction()) .event(Events.VIEW_I) .and() .withExternal() .source(States.ITEMS).target(States.CART) .action(pageviewAction()) .event(Events.VIEW_C) .and() .withExternal() .source(States.PAYMENT).target(States.CART) .action(pageviewAction()) .event(Events.VIEW_C) .and() .withExternal() .source(States.CART).target(States.PAYMENT) .action(pageviewAction()) .event(Events.VIEW_P) .and() .withExternal() .source(States.ITEMS).target(States.HOME) .action(resetAction()) .event(Events.RESET) .and() .withExternal() .source(States.CART).target(States.HOME) .action(resetAction()) .event(Events.RESET) .and() .withExternal() .source(States.PAYMENT).target(States.HOME) .action(resetAction()) .event(Events.RESET); return builder.build(); }
In below config we set up a RedisConnectionFactory
which defaults to
localhost and default port. We use StateMachinePersist
with a
RepositoryStateMachinePersist
implementation. Finally we create a
RedisStateMachinePersister
which underneath uses a previously
created StateMachinePersist
bean.
These are then used in a Controller
handling REST
calls.
@Bean public RedisConnectionFactory redisConnectionFactory() { return new JedisConnectionFactory(); } @Bean public StateMachinePersist<States, Events, String> stateMachinePersist(RedisConnectionFactory connectionFactory) { RedisStateMachineContextRepository<States, Events> repository = new RedisStateMachineContextRepository<States, Events>(connectionFactory); return new RepositoryStateMachinePersist<States, Events>(repository); } @Bean public RedisStateMachinePersister<States, Events> redisStateMachinePersister( StateMachinePersist<States, Events, String> stateMachinePersist) { return new RedisStateMachinePersister<States, Events>(stateMachinePersist); }
We now get into why StateMachine
was created as stateMachineTarget
and a prototype
bean. State machine instantiation is a relatively
expensive operation so it is better to try to pool instances instead
of instantiating a new instance with every request. For this we first
create a poolTargetSource
which wraps stateMachineTarget
and pools
it with max size of 3. This poolTargetSource
is then proxied with
ProxyFactoryBean
using a request
scope. Effectively this means
that every REST
request will get pooled state machine instance from
a bean factory. It’s shown later how these are used.
@Bean @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) public ProxyFactoryBean stateMachine() { ProxyFactoryBean pfb = new ProxyFactoryBean(); pfb.setTargetSource(poolTargetSource()); return pfb; }
@Bean public CommonsPool2TargetSource poolTargetSource() { CommonsPool2TargetSource pool = new CommonsPool2TargetSource(); pool.setMaxSize(3); pool.setTargetBeanName("stateMachineTarget"); return pool; }
Let’s get into actual demo. You need to have a redis running on a localhost with a default settings. Then run the boot based sample application:
@n1:~# java -jar spring-statemachine-samples-eventservice-2.1.1.RELEASE.jar
In a browser you see something like:
In this UI you have three users you can use, joe
, bob
and dave
.
Clicking button will show current state and extended state. Enabling a
radio button before clicking users will send particular event for that
user. This is a way you can play with this using an UI.
In our StateMachineController
we autowire StateMachine
and
StateMachinePersist
. StateMachine
is a request
scoped so you’ll
get new instance per request while StateMachinePersist
is normal
singleton bean.
@Autowired private StateMachine<States, Events> stateMachine; @Autowired private StateMachinePersister<States, Events, String> stateMachinePersister;
Below feedAndGetState
is just used with an UI to do same things what
actual REST
api will do.
@RequestMapping("/state") public String feedAndGetState(@RequestParam(value = "user", required = false) String user, @RequestParam(value = "id", required = false) Events id, Model model) throws Exception { model.addAttribute("user", user); model.addAttribute("allTypes", Events.values()); model.addAttribute("stateChartModel", stateChartModel); // we may get into this page without a user so // do nothing with a state machine if (StringUtils.hasText(user)) { resetStateMachineFromStore(user); if (id != null) { feedMachine(user, id); } model.addAttribute("states", stateMachine.getState().getIds()); model.addAttribute("extendedState", stateMachine.getExtendedState().getVariables()); } return "states"; }
Below feedPageview
is a REST
method which accepts a post with a
json content.
@RequestMapping(value = "/feed",method= RequestMethod.POST) @ResponseStatus(HttpStatus.OK) public void feedPageview(@RequestBody(required = true) Pageview event) throws Exception { Assert.notNull(event.getUser(), "User must be set"); Assert.notNull(event.getId(), "Id must be set"); resetStateMachineFromStore(event.getUser()); feedMachine(event.getUser(), event.getId()); }
Below feedMachine
will send event into a StateMachine
and persists
its state using a StateMachinePersister
.
private void feedMachine(String user, Events id) throws Exception { stateMachine.sendEvent(id); stateMachinePersister.persist(stateMachine, "testprefix:" + user); }
Below resetStateMachineFromStore
is used to restore a state machine
for a particular user.
private StateMachine<States, Events> resetStateMachineFromStore(String user) throws Exception { return stateMachinePersister.restore(stateMachine, "testprefix:" + user); }
As you’d send event using UI, same can be done using a REST
calls:
# curl http://localhost:8080/feed -H "Content-Type: application/json" --data '{"user":"joe","id":"VIEW_I"}'
At this point you should have a content in Redis
with a
testprefix:joe
key.
$ ./redis-cli 127.0.0.1:6379> KEYS * 1) "testprefix:joe"
Below is a three images when state for joe
has been changed from
HOME
to ITEMS
and when ADD
action has been executed.
Send event ADD
:
Now your are still on state ITEMS
and internal transition caused
extended state variable COUNT
to increase to 1
.
Execute below curl
rest call few times or do it via UI and you
should see COUNT
variable to increase with every call.
# curl http://localhost:8080/feed -H "Content-Type: application/json" # --data '{"user":"joe","id":"ADD"}'
Deploy is an example how state machine concepts can be used with an uml modeling to provide a generic error handling state. This state machine is a relatively complex example of how various features can be used to provide a centralized error handling concept.
Note | |
---|---|
Above statechart is designed using Eclipse Papyrus Plugin Chapter 33, Eclipse Modeling Support and imported into Spring StateMachine via its uml model file. Actions and Guards defined in a model are resolved from a Spring Application Context. |
In this state machine scenario we have two different behaviors,
DEPLOY
and UNDEPLOY
what user tries to execute.
What is happening a above statechart:
DEPLOY
state INSTALL
and START
states are entered
conditionally. We enter START
directly if product is already
installed and no need to try to START
if install fails.
UNDEPLOY
state we enter STOP
conditionally if application is
already running.
DEPLOY
and UNDEPLOY
are done via
Choice pseudostate within those states and choices are selected
by Guards.
DEPLOY
and UNDEPLOY
states.
DEPLOY
and UNDEPLOY
we go through a Junction
pseudostate to make a choice if we want to go through ERROR
state
in case error was added into an Extended State.
READY
state to process new requests.
Let’s get into actual demo. Run the boot based sample application:
# java -jar spring-statemachine-samples-deploy-2.1.1.RELEASE.jar
In a browser you see something like:
Important | |
---|---|
As we don’t have a real install, start or stop functionality we simulate failures by checking existence of particular message headers. |
Now you can start to send event to a machine and choose various message headers which will drive a different functionality.
Order Shipping is an example how state machine concepts can be used with a process of building a simple order processing system.
Below you can see a statechart driving this order shipping sample.
What is happening a above statechart:
WAIT_NEW_ORDER
default state.
PLACE_ORDER
transitions into state RECEIVE_ORDER
and entry
action entryReceiveOrder
is executed.
CUSTOMER_ERROR
final state.
RECEIVE_PAYMENT
is sent successfully to indicate correct
payment.
WAIT_PRODUCT
and
WAIT_ORDER
to be joined before parent orthogonal state
HANDLE_ORDER
is exited.
SHIP_ORDER
into its final state
ORDER_SHIPPED
.
Let’s get into actual demo. Run the boot based sample application:
# java -jar spring-statemachine-samples-ordershipping-2.1.1.RELEASE.jar
In a browser you see something shown above. You can start by choosing customer and order and create a machine.
Machine for particular order is now created and you can start to play with placing an order and sending a payment. Other settings like makeProdPlan, produce and payment allows you to control how machine works.
Finally you can see what machine does by refreshing a page.
JPA Config is an example how state machine concepts can be used with a machine configuration kept in a database. This sample is using embedded H2 database with a H2 Console to ease playing with a database.
This sample uses spring-statemachine-autoconfigure
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); } }
What comes for a machine config RepositoryStateMachineModelFactory
can be used as shown below.
@Configuration @EnableStateMachineFactory public static class Config extends StateMachineConfigurerAdapter<String, String> { @Autowired private StateRepository<? extends RepositoryState> stateRepository; @Autowired private TransitionRepository<? extends RepositoryTransition> transitionRepository; @Override public void configure(StateMachineModelConfigurer<String, String> model) throws Exception { model .withModel() .factory(modelFactory()); } @Bean public StateMachineModelFactory<String, String> modelFactory() { return new RepositoryStateMachineModelFactory(stateRepository, transitionRepository); } }
Let’s get into actual demo. Run the boot based sample application:
# java -jar spring-statemachine-samples-datajpa-2.1.1.RELEASE.jar
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.
To access embedded console use JDBC URL jdbc:h2:mem:testdb
if it’s
not already set.
From console you can see how database tables look like and modify those as you wish.
Now that you got this far you probably wondered how those default
states and transitions got populated into a database. Spring Data
already have a nice trick to auto populate repositories and we simply
use this feature via Jackson2RepositoryPopulatorFactoryBean
.
@Bean public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() { StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean(); factoryBean.setResources(new Resource[]{new ClassPathResource("data.json")}); return factoryBean; }
Actual source for populator data is shown below.
[ { "@id": "10", "_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction", "spel": "T(System).out.println('hello exit S1')" }, { "@id": "11", "_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction", "spel": "T(System).out.println('hello entry S2')" }, { "@id": "12", "_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction", "spel": "T(System).out.println('hello state S3')" }, { "@id": "13", "_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction", "spel": "T(System).out.println('hello')" }, { "@id": "1", "_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState", "initial": true, "state": "S1", "exitActions": ["10"] }, { "@id": "2", "_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState", "initial": false, "state": "S2", "entryActions": ["11"] }, { "@id": "3", "_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState", "initial": false, "state": "S3", "stateActions": ["12"] }, { "_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition", "source": "1", "target": "2", "event": "E1", "kind": "EXTERNAL" }, { "_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition", "source": "2", "target": "3", "event": "E2", "actions": ["13"] } ]
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-autoconfigure
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-2.1.1.RELEASE.jar
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-2.1.1.RELEASE.jar --spring.profiles.active=jpa # java -jar spring-statemachine-samples-datapersist-2.1.1.RELEASE.jar --spring.profiles.active=mongo # java -jar spring-statemachine-samples-datapersist-2.1.1.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.
Sample defaults to using machine 'datajpapersist1' and goes to its initial state 'S1'.
If events 'E1' and 'E2' are sent into machine 'datajpapersist1' its state is persisted as 'S3'.
If requesting machine 'datajpapersist1' by not sending any events, machine is restored back to its persisted state 'S3'.
Data Multi Persist is an example which is an extension of two other samples Chapter 50, JPA Config and Chapter 51, Data Persist. We still keep machine configuration in a database and persist into a database but this time we also have a machine containing two orthogonal regions showing how those are persisted independently. This sample is also using embedded H2 database with a H2 Console to ease playing with a database.
This sample uses spring-statemachine-autoconfigure
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); } }
We again create a StateMachineRuntimePersister
.
@Bean public StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister( JpaStateMachineRepository jpaStateMachineRepository) { return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository); }
StateMachineService
makes it easier to work with a machines.
@Bean public StateMachineService<String, String> stateMachineService( StateMachineFactory<String, String> stateMachineFactory, StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister) { return new DefaultStateMachineService<String, String>(stateMachineFactory, stateMachineRuntimePersister); }
We use data from json to import configuration.
@Bean public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() { StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean(); factoryBean.setResources(new Resource[] { new ClassPathResource("datajpamultipersist.json") }); return factoryBean; }
What comes for a machine config RepositoryStateMachineModelFactory
can be used as shown below.
@Configuration @EnableStateMachineFactory public static class Config extends StateMachineConfigurerAdapter<String, String> { @Autowired private StateRepository<? extends RepositoryState> stateRepository; @Autowired private TransitionRepository<? extends RepositoryTransition> transitionRepository; @Autowired private StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister; @Override public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception { config .withPersistence() .runtimePersister(stateMachineRuntimePersister); } @Override public void configure(StateMachineModelConfigurer<String, String> model) throws Exception { model .withModel() .factory(modelFactory()); } @Bean public StateMachineModelFactory<String, String> modelFactory() { return new RepositoryStateMachineModelFactory(stateRepository, transitionRepository); } }
Let’s get into actual demo. Run the boot based sample application:
# java -jar spring-statemachine-samples-datajpamultipersist-2.1.1.RELEASE.jar
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. We also print out all state machine contexts and current root machine.
Machine datajpamultipersist1 is simple flat machine where states S1, S2 and S3 are transitioned with events E1, E2 and E3 meaning nothing new there. However machine datajpamultipersist2 contains two regions R1 and R2 directly under root level, thus a reason why root level machine really doesn’t have a state at all but we still need that root level machine to host those regions.
Regions R1 and R2 in machine datajpamultipersist2 contains states S10, S11, S12 and S20, S21, S22 respectively and events E10, E11 and E12 are used for region R1 and events E20, E21 and E22 for region R2. Lets send events E10 and E20 to machine datajpamultipersist2 and see how things look like.
Regions have their own contexts with their own id’s and where the actual
id is postfixed with #
plus region id
. As shown below there are
different contexts in a database for regions.
Monitoring is an example how state machine concepts can be used to monitor machine transitions and actions.
@Configuration @EnableStateMachine public static class Config extends StateMachineConfigurerAdapter<String, String> { @Override public void configure(StateMachineStateConfigurer<String, String> states) throws Exception { states .withStates() .initial("S1") .state("S2", null, (c) -> {System.out.println("hello");}) .state("S3", (c) -> {System.out.println("hello");}, null); } @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions .withExternal() .source("S1").target("S2").event("E1") .action((c) -> {System.out.println("hello");}) .and() .withExternal() .source("S2").target("S3").event("E2"); } }
Let’s get into actual demo. Run the boot based sample application:
# java -jar spring-statemachine-samples-monitoring-2.1.1.RELEASE.jar
Execute some transitions.
Metrics can be viewed from Boot.
# curl http://localhost:8080/actuator/metrics/ssm.transition.duration { "name":"ssm.transition.duration", "measurements":[ { "statistic":"COUNT", "value":3.0 }, { "statistic":"TOTAL_TIME", "value":0.007 }, { "statistic":"MAX", "value":0.004 } ], "availableTags":[ { "tag":"transitionName", "values":[ "INITIAL_S1", "EXTERNAL_S1_S2" ] } ] }
# curl http://localhost:8080/actuator/metrics/ssm.transition.transit { "name":"ssm.transition.transit", "measurements":[ { "statistic":"COUNT", "value":3.0 } ], "availableTags":[ { "tag":"transitionName", "values":[ "EXTERNAL_S1_S2", "INITIAL_S1" ] } ] }
Tracing can be viewed from Boot.
# curl http://localhost:8080/actuator/statemachinetrace [ { "timestamp":"2018-02-11T06:44:12.723+0000", "info":{ "duration":2, "machine":null, "transition":"EXTERNAL_S1_S2" } }, { "timestamp":"2018-02-11T06:44:12.720+0000", "info":{ "duration":0, "machine":null, "action":"demo.monitoring.StateMachineConfig$Config$$Lambda$576/1499688007@22b47b2f" } }, { "timestamp":"2018-02-11T06:44:12.714+0000", "info":{ "duration":1, "machine":null, "transition":"INITIAL_S1" } }, { "timestamp":"2018-02-11T06:44:09.689+0000", "info":{ "duration":4, "machine":null, "transition":"INITIAL_S1" } } ]
This chapter tries to give solutions to question user is most likely to ask.
I want to transit to next state automatically.
There are few choices a state machine developer can choose.
How I can initialise variables on state machine start.
Important concept in a state machine is that nothing really happens unless there is a trigger which is causing a state transition which then can fire actions. However, having said that, Spring Statemachine always have an initial transition when state machine is started. With this initial transition user can execute a simple action which within a StateContext can do whatever it likes with an extended state variables.
This appendix provides generic information about used classes and material in this reference documentation.
This appendix provides generic information about state machines.
Assuming we have states STATE1, STATE2 and events EVENT1, EVENT2, logic of state machine can be defined as shown in below quick example.
public enum States { STATE1, STATE2 } public enum Events { EVENT1, EVENT2 }
@Configuration @EnableStateMachine public class Config1 extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception { states .withStates() .initial(States.STATE1) .states(EnumSet.allOf(States.class)); } @Override public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception { transitions .withExternal() .source(States.STATE1).target(States.STATE2) .event(Events.EVENT1) .and() .withExternal() .source(States.STATE2).target(States.STATE1) .event(Events.EVENT2); } }
@WithStateMachine public class MyBean { @OnTransition(target = "STATE1") void toState1() { } @OnTransition(target = "STATE2") void toState2() { } }
public class MyApp { @Autowired StateMachine<States, Events> stateMachine; void doSignals() { stateMachine.sendEvent(Events.EVENT1); stateMachine.sendEvent(Events.EVENT2); } }
This appendix provides generic crash course to a state machine concepts.
A state is a model which a state machine can be in. It is always easier to describe state as a real world example rather than trying to abstract concepts with a generic documentation. For example let’s take a simple example of a keyboard most of us are using every single day. If you have a full keyboard which has normal keys on a left side and the numeric keypad on a right side you may have noticed that the numeric keypad may be in a two different states depending whether numlock is activated or not. If it is not active then typing will result navigation using arrows, etc. If numpad is active then typing will result numbers to be used. Essentially numpad part of a keyboard can be in two different states.
To relate state concept to programming it means that instead of using flags, nested if/else/break clauses or other impractical logic you simply rely on state, state variables or other interaction with a state machine.
PseudoState is a special type of state which usually introduces more higher level logic into a state machine by either giving a state a special meaning like initial state. State machine can then internally react to these states by doing various actions available in UML state machine concepts.
Initial pseudostate state is always needed for every single state machine whether you have a simple one level state machine or more complex state machine composed with submachines or regions. Initial state simple defines where state machine should go when it starts and without it state machine is ill-formed.
Terminate pseudostate which is also called as end state will indicate that a particular state machine has reached its final state. Effectively this mean that a state machine will no longer process any events and will not transit to any other state. However in a case of submachines are regions, state machine is able to restart from its terminal state.
Choice pseudostate is used to choose a dynamic conditional branch of a transition from this state. Dynamic condition is evaluated by guards so that at least one and at most one branch is selected. Usually a simple if/elseif/else structure is used to make sure that at least one branch is selected. Otherwise state machine might end up in a deadlock and configuration would be ill-formed.
Junction pseudostate is functionally similar than choice as both are implemented with if/elseif/else structure. Only real difference is that junction allows multiple incoming transitions while choice only allows one. Thus difference is purely academic but have some differences i.e. when state machine is designed using real UI modeling framework.
History pseudostate can be used to remember a last active state configuration. After state machine has been exited, history state can be used to restore previous knows configuration. There are two types of history states available, SHALLOW only remember active state of a state machine itself while DEEP also remembers nested states.
History state could be implemented externally by listening state machine events but this would soon make logic very difficult to work with, especially if state machine contains complex nested structures. Letting state machine itself to handle recording of history states makes things much simpler. What is left for user to do is simply do a transition into a history state and state machine will hand the needed logic to go back to its last known recorded state.
In cases where a Transition terminates on a history state when the state has not been entered before (i.e., no prior history) or it had reached its End State, there is an option to force a transition to a specific substate, using the default history mechanism. This is a Transition that originates in the history state and terminates on a specific Vertex (the default history state) of the Region containing the history state. This Transition is only taken if execution leads to the history state and the state had never been active before. Otherwise, the normal history entry into the Region is executed. If no default history transition is defined, then standard default entry of the region is performed.
Fork pseudostate can be used to do an explicit entry into one or more regions.
Target state can be a parent state hosting regions, which simply means that regions are activated by entering its initial states. It’s also possible to add targets directly to any state in a region which allows more controlled entry into a state.
Join pseudostate is used to merge several transitions together originating from different regions. It is generally used to wait and block for participating regions to get into its join target states.
Source state can be a parent state hosting regions, which means that join states will be a terminate states of a participating regions. It’s also possible to define source states to be any state in a regions which allows controlled exit from a regions.
An Entry Point pseudostate represents an entry point for a state machine or a composite state that provides encapsulation of the insides of the state or state machine. In each region of the state machine or composite state owning the Entry Point , there is at most a single transition from the entry point to a Vertex within that Region.
An Exit Point pseudostate is an exit point of a state machine or composite state that provides encapsulation of the insides of the state or state machine. Transitions terminating on an Exit Point within any region of the composite state or a state machine referenced by a submachine state implies exiting of this composite state or submachine state (with execution of its associated exit behavior).
Guard conditions are expressions which evaluates either to TRUE or FALSE based on extended state variables and event parameters. Guards are used with actions and transitions to dynamically choose if particular action or transition should be executed. Aspects of guards, event parameters and extended state variables are simply to make state machine design much more simple.
Event is the most used trigger behaviour to drive a state machine. There are other ways to trigger behaviour to happen in state machine like a timer but events are the ones which really allows user to interact with a state machine. Events are also called as signals to possibly alter a state machine state.
A transition is a relationship between a source state and a target state. A switch from a state to another is a state transition caused by a trigger.
Internal transition is used when action needs to be executed without causing a state transition. With internal transition source and target state is always a same and it is identical with self-transition in the absence of state entry and exit actions.
Most of the cases external and local transition are functionally equivalent except in cases where transition is happening between super and sub states. Local transition doesn’t cause exit and entry to source state if target state is a substate of a source state. Other way around, local transition doesn’t cause exit and entry to target state if target is a superstate of a source state.
Above image shows a different between local and external transitions with a very simplistic super and sub states.
Actions are the ones which really glues state machine state changes with a user’s own code. State machine can execute action on various changes and steps in a state machine like entering or exiting a state, or doing a state transition.
Actions usually have access to a state context which gives running code a choice to interact with a state machine in a various ways. State context i.e. is exposing a whole state machine so user can access extended state variables, event headers if transition is based on an event, or actual transition where it is possible to see more detailed where this state change is coming from and where it is going.
Concept of a hierarchical state machine is used to simplify state design when particular states can only exist together.
Hierarchical states are really an innovation in UML state machine over a traditional state machines like Mealy or Moore machines. Hierarchical states allows to define some level of abstraction is a sense how java developer would define a class structure with abstract classes. For example having a nested state machine user is able to define transition on a multiple level of states possibly with a different conditions. State machine will always try to see if current state is able to handle an event together with a transition guard conditions. If these conditions are not evaluated to true, state machine will simply see what a super state can handle.
Regions which are also called as orthogonal regions are usually viewed as exclusive OR operation applied to a states. Concept of a region in terms of a state machine is usually a little difficult to understand but things gets a little simpler with a simple example.
Some of us have a full size keyboard with main keys on a left side and numeric keys on a right side. You’ve probably noticed that both sides really have their own state which you see if you press a numlock key which only alters behaviour of numpad itself. If you don’t have a full size keyboard you can buy a simple external usb numpad having only numpad part of a keys. If left and right side can freely exist without the other they must have a totally different states which means they are operating on different state machines.
It would be a little inconvenient to handle two different state machines as totally separate entities because in a sense they are still working together in a sense. This is why orthogonal regions can combine together a multiple simultaneous states within a single state in a state machine.
This appendix provides more detailed technical documentation about using a Zookeeper with a Spring State Machine.
Introducing a distributed state
on top of a single state machine
instance running on a single jvm
is a difficult and a complex topic.
Distributed State Machine
is introducing a few relatively complex
problems on top of a simple state machine due to its run-to-completion
model and generally because of its single thread execution model,
though orthogonal regions can be executed parallel. One other natural
problem is that a state machine transition execution is driven by triggers
which are either event
or timer
based.
Distributed Spring State Machine
is trying to solve problem of spanning
a generic State Machine
through a jvm boundary. Here we show that a generic
State Machine
concepts can be used in multiple jvm’s
and Spring
Application Contexts
.
We found that if Distributed State Machine
abstraction is carefully chosen
and backing distributed state repository guarantees CP
readiness, it is
possible to create a consistent state machine which is able to share
distributed state among other state machines in an ensemble.
Our results demonstrate that distributed state changes are consistent if backing
repository is CP
. We anticipate our distributed state machine to provide
a foundation to applications which need to work with a shared distributed
states. This model aims to provide a good methods for cloud applications
to have much easier ways to communicate with each others without having
a need to explicitly build these distributed state concepts.
Spring State Machine is not forced to use a single threaded execution model because once multiple regions are uses, regions can be executed parallel if necessary configuration is applied. This is an important topic because once user wants to have a parallel state machine execution it will make state changes faster for independent regions.
When state changes are no longer driven by a trigger in a local jvm or local state machine instance, transition logic needs to be controlled externally in an arbitrary persistent storage. This storage needs to have a ways to notify participating state machines when distributed state is changed.
CAP Theorem states that
"it is impossible for a distributed computer system to simultaneously
provide all three of the following guarantees, consistency
,
availability
and partition tolerance
". What this means is that
whatever is chosen for a backing persistence storage is it advisable
to be CP
. In this context CP
means consistency
and partition
tolerance
. Naturally Distributed Spring Statemachine
doesn’t care
about what is its CAP
level but in reality consistency
and
partition tolerance
are more important than availability
. This is
an exact reason why i.e. Zookeeper
is a CP
storage.
All tests presented in this article are accomplished by running custom
jepsen
tests in a following environment:
Zookeeper
instance constructing an ensemble with
all other nodes.
Zookeeper
node.
Zookeeper
instance. While connecting machine to multiple instances
is possible, it is not used here.
StateMachineEnsemble
using Zookeeper
ensemble.
jepsen
will use to send
events and check particular state machine statuses.
All jepsen tests for Spring Distributed Statemachine
are available from
Jepsen
Tests.
One design decision of a Distributed State Machine
was not to make
individual State Machine
instance aware of that it is part of a
distributed ensemble
. Because main functions and features of a
StateMachine
can be accessed via its interface, it makes sense to
wrap this instance using a DistributedStateMachine
, which simply
intercepts all state machine communication and collaborates with an
ensemble to orchestrate distributed state changes.
One other important concept is to be able to persist enough
information from a state machine order to reset a state machine state
from arbitrary state into a new deserialized state. This is naturally
needed when a new state machine instance is joining with an ensemble
and it needs to synchronize its own internal state with a distributed
state. Together with using concepts of distributed states and state
persisting it is possible to create a distributed state machine.
Currently only backing repository of a Distributed State Machine
is
implemented using a Zookeeper
.
As mentioned in Chapter 31, Using Distributed States distributed states are enabled by
wrapping an instance of a StateMachine
within a
DistributedStateMachine
. Specific StateMachineEnsemble
implementation is ZookeeperStateMachineEnsemble
providing
integration with a Zookeeper
.
We wanted to have a generic interface StateMachinePersist
which is
able to persist StateMachineContext
into an arbitrary storage and
ZookeeperStateMachinePersist
is implementing this interface for a
Zookeeper
.
While distributed state machine is using one set of serialized
contexts to update its own state, with zookeeper we’re having a
conceptual problem how these context changes can be listened. We’re
able to serialize context into a zookeeper znode
and eventually
listen when znode
data is modified. However Zookeeper
doesn’t
guarantee that you will get notification for every data change
because registered watcher
for a znode
is disabled once it fires
and user need to re-register that watcher
. During this short time
a znode
data can be changed thus resulting missing events. It is
actually very easy to miss these events by just changing data from a
multiple threads in a concurrent manner.
Order to overcome this issue we’re keeping individual context changes
in a multiple znodes
and we just use a simple integer counter to mark
which znode
is a current active one. This allows us to replay missed
events. We don’t want to create more and more znodes and then later
delete old ones, instead we’re using a simple concept of a circular
set of znodes. This allows to use predefined set of znodes where
a current can be determined with a simple integer counter. We already have
this counter by tracking main znode
data version which in
Zookeeper
is
an integer.
Size of a circular buffer is mandated to be a power of two not to get trouble when integer is going to overflow thus we don’t need to handle any specific cases.
Order to show how a various distributed actions against a state
machine work in a real life, we’re using a set of jepsen
tests to
simulate various conditions which may happen in a real distributed
cluster. These include a brain split
on a network level, parallel
events with a multiple distributed state machines
and changes in
an extended state variables
. Jepsen tests are based on a sample
Chapter 44, Web where this sample instance is run on
multiple hosts together with a Zookeeper
instance on every node
where state machine is run. Essentially every state machine sample
will connect to local Zookeeper
instance which allows use, via
jepsen
to simulate network conditions.
Plotted graphs below in this chapter contain states and events which directly maps to a state chart which can be found from Chapter 44, Web.
Sending an isolated single event into exactly one state machine in an ensemble is the most simplest testing scenario and demonstrates that a state change in one state machine is properly propagated into other state machines in an ensemble.
In this test we will demonstrate that a state change in one machine will eventually cause a consistent state change in other machines.
What’s happening in above chart:
S21
.
I
is sent to node n1
and all nodes report state change
from S21
to S22
.
C
is sent to node n2
and all nodes report state change
from S22
to S211
.
I
is sent to node n5
and all nodes report state change
from S211
to S212
.
K
is sent to node n3
and all nodes report state change
from S212
to S21
.
I
, C
, I
and K
one more time via random nodes.
Logical problem with multiple distributed state machines is that if a same event is sent into a multiple state machine exactly at a same time, only one of those events will cause a distributed state transitions. This is somewhat expected scenario because a first state machine, for this event, which is able to change a distributed state will control the distributed transition logic. Effectively all other machines receiving this same event will silently discard the event because distributed state is no longer in a state where particular event can be processed.
In this test we will demonstrate that a state change caused by a parallel events throughout an ensemble will eventually cause a consistent state change in all machines.
What’s happening in above chart:
Extended state machine variables are not guaranteed to be atomic at any given time but after a distributed state change, all state machines in an ensemble should have a synchronized extended state.
In this test we will demonstrate that a change in extended state variables in one distributed state machine will eventually be consistent in all distributed state machines.
What’s happening in above chart:
J
is send to node n5
with event variable testVariable
having value v1
. All nodes are then reporting having variable
testVariable
as value v1
.
J
is repeated from variable v2
to v8
doing same checks.
We need to always assume that sooner or later things in a cluster will
go bad whether it is just a crash of a Zookeeper
instance, a state
machine or a network problem like a brain split
. Brain split is a
situation where existing cluster members are isolated so that only
part of a hosts are able to see each others. Usual scenario is that a
brain split will create a minority and majority partitions of an
ensemble where hosts in a minority cannot participate in an ensemble
anymore until network status has been healed.
In below tests we will demonstrate that various types of brain-split’s in an ensemble will eventually cause fully synchronized state of all distributed state machines.
There are two scenarios having a one straight brain split in a
network where where Zookeeper
and Statemachine
instances are
split in half, assuming each Statemachine
will connect into a
local Zookeeper
instance:
Note | |
---|---|
In our current |
Note | |
---|---|
In below plots we have mapped a state machine error state into an
|
In this first test we show that when existing zookeeper leader was kept in majority, 3 out of 5 machines will continue as is.
What’s happening in above chart:
C
is sent to all machine leading a state change to
S211
.
n1/n2/n5
and n3/n4
. Nodes n3/n4
are left in minority and
nodes n1/n2/n5
construct a new healthy majority. Nodes in
majority will keep function without problems but nodes in minority
will get into error state.
n3/n4
will join
back into ensemble and synchronize its distributed status.
K1
is sent to all state machines to ensure that ensemble
is working properly. This state change will lead back to state
S21
.
In this second test we show that when existing zookeeper leader was kept in minority, all machines will error out:
What’s happening in above chart:
C
is sent to all machine leading a state change to
S211
.
Zookeeper
leader is kept in minority and all
instances are disconnected from ensemble.
K1
is sent to all state machines to ensure that ensemble
is working properly. This state change will lead back to state
S21
.
In this test we will demonstrate that killing existing state machine and then joining new instance back into an ensemble will keep the distributed state healthy and newly joined state machines will synchronize their states properly.
Note | |
---|---|
In this test, states are not checked between first |
What’s happening in above chart:
S21
into
S211
so that we can test proper state synchronize during join.
X
is marking when a specific node has been crashed and started.
S21
from S211
to make
sure that all state machines are still functioning properly.
This appendix provides generic information for a developers who may want to contribute or other people who want to understand how state machine works or what are its internal concepts.
StateMachineModel
and other related SPI classes are an abstraction
between various configuration and factory classes. This also allows
easier integration for others to build state machines.
As shown above a state machine can be instantiated by building a model using configuration data classes and then asking a factory to build a state machine.
// setup configuration data ConfigurationData<String, String> configurationData = new ConfigurationData<>(); // setup states data Collection<StateData<String, String>> stateData = new ArrayList<>(); stateData.add(new StateData<String, String>("S1", true)); stateData.add(new StateData<String, String>("S2")); StatesData<String, String> statesData = new StatesData<>(stateData); // setup transitions data Collection<TransitionData<String, String>> transitionData = new ArrayList<>(); transitionData.add(new TransitionData<String, String>("S1", "S2", "E1")); TransitionsData<String, String> transitionsData = new TransitionsData<>(transitionData); // setup model StateMachineModel<String, String> stateMachineModel = new DefaultStateMachineModel<>(configurationData, statesData, transitionsData); // instantiate machine via factory ObjectStateMachineFactory<String, String> factory = new ObjectStateMachineFactory<>(stateMachineModel); StateMachine<String, String> stateMachine = factory.getStateMachine();