1.0.0.M3
Copyright © 2015 Pivotal Software, Inc.
Table of Contents
Concept of a state machine is most likely older that any of a reader of this reference documentation and definitely older than a Java language itself. Description of finite automate dates back to 1943 when gentlements 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’re ever read anything about state machines, names Mealy and Moore should have popped up at some point.
This reference documentations contains following parts.
Part I, “Introduction” introduction to this reference documentation
Part II, “Spring and Statemachine” describes the usage of Spring State Machine(SSM)
Part IV, “State Machine Examples” more detailed state machine samples
Part V, “FAQ” frequently ask questions
Part VI, “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 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 rest of a documentation expects reader to be fairly familiar with state machine concepts.
Spring Statemachine 1.0.0.M3 is built and tested with JDK 7 and Spring Framework 4.1.6.RELEASE and doesn’t require any other dependencies outside of Spring Framework. Samples require spring-shell and spring-boot which pulls other dependencies beyond framework itself.
State machines are powerful because behaviour is always guaranteed to be consistent and relatively easily debugged due to ways how operational rules are written in stone when machine is started. 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 with a various different ways. You can simply interact with a state machine by sending event, listening what a state machine does or simply request a current state.
Traditionally state machines are added to a existing project when developer realizes that code base is starting to look like a plate full of spaghetti. Spaghetti code looks like never ending hierarchical structure of IFs, ELSEs and BREAK clauses and probably compiler should ask developer to go home when things are starting to look too complex.
Project is a good candidate to use state machine if:
You are already trying to implement a state machine if:
This part of the reference documentation explains the core functionality that Spring Statemachine provides to any Spring based application.
Chapter 4, Statemachine Configuration describes the generic configuration support.
Chapter 5, State Machine Factories describes the generic state machine factory support.
Chapter 9, Triggering Transitions describes the use of triggers.
Chapter 10, Listening State Machine Events describes the use of state machine listeners.
Chapter 11, Context Integration describes the generic Spring application context 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’ll get into more complex configuration examples a bit later but
lets 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 static 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 static 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 static 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 static 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); } }
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 static 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 static 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 spel
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 with various steps within a state transitions.
@Configuration @EnableStateMachine public static class Config5 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 } }; } }
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 static 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 static 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 static 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); } }
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 static 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; } }; } }
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 static 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 one 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 static 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) .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.S5); } }
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 static class Config17 extends EnumStateMachineConfigurerAdapter<States, Events> { @Override public void configure(StateMachineConfigurationConfigurer<States, Events> config) throws Exception { config .withConfiguration() .autoStartup(true) .beanFactory(new StaticListableBeanFactory()) .taskExecutor(new SyncTaskExecutor()) .taskScheduler(new ConcurrentTaskScheduler()) .listener(new StateMachineListenerAdapter<States, Events>()); } }
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 a BeanFactory
, TaskExecutor
or TaskScheduler
exist for
conveniance 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.
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 static 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 16, Using Distributed States.
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 static 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.
static 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 patter can be
used to construct similar instances. Using strings as states and
events this builder patter can be used to build fully dynamic state
machines outside of a Spring application context as shown above.
StateMachine<String, String> buildMachine() 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 |
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)")); } static class BaseAction implements Action<States, Events> { @Override public void execute(StateContext<States, Events> context) { } } static 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 8, 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(); } static class BaseGuard implements Guard<States, Events> { @Override public boolean evaluate(StateContext<States, Events> context) { return false; } }
Note | |
---|---|
StateContext is described in section Chapter 8, 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.
StateContext is a domain object representing a current status of a state machine within a transition or an action. Context gives an access to a various information like event, message headers, extended state variables, current transition and a top-level state machine in case there is a need to send events to a further processing.
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.
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. These can be used as is with spring typed ApplicationListener class but they also share a common class StateMachineEvent which can be used to get statemachine related events.
static class StateMachineApplicationEventListener implements ApplicationListener<StateMachineEvent> { @Override public void onApplicationEvent(StateMachineEvent event) { } }
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.
static 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) { } }
In above example we simply created our own listener class StateMachineEventListener which extends StateMachineListenerAdapter.
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.
static 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 event bus 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 static class Config8 extends EnumStateMachineConfigurerAdapter<States, Events> { } @Configuration @EnableStateMachineFactory(contextEvents = false) public static 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.
@WithStateMachine annotation can be used to associate a state machine with a existing bean. Withing this annotation a propertys source and target can be used to qualify a transition
@WithStateMachine static class Bean1 { @OnTransition(source = "S1", target = "S2") public void fromS1ToS2() { } }
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.
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 static @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 static class Bean2 { @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
.
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 17, Persist and one sample Chapter 24, 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 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 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 Exception stateMachineError(StateMachine<String, String> stateMachine, Exception exception) { return exception; } });
Note | |
---|---|
More about error handling shown in above example, see section Chapter 14, State Machine Error Handling. |
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 change 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 10, Listening State Machine Events.
Having said that, a simple listener would look like:
public static 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 static 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 static class ErrorApplicationEventListener implements ApplicationListener<OnStateMachineError> { @Override public void onApplicationEvent(OnStateMachineError event) { // do something with error } }
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 17, Persist and one sample Chapter 24, 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 13, State Machine Interceptor.
Distributed state is probably one of a most compicated 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 thoughout 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 configuration support see section Section 4.8, “Configuring Common Settings” and actual usage example see sample Chapter 25, 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 enought information to reset a
StateMachine
.
While Distributed State Machine
is implemented via an abstraction,
only one implementation currently exists based on Zookeeper
.
Current technical documentation of a Zookeeker
based distributed
state machine can be found from an appendice Appendix C, Distributed State Machine with Zookeeper.
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.
Recipe’s 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 24, 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 22, 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
hierarcical 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 presise.
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 maching 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 patter. This builder can be used to
register Runnable
tasks, TasksListener
instances, define
StateMachinePersist
hook, and setup custom TaskExecutor
instance.
Now lets take a simple Runnable
just doing a simple sleep as shown
below. This is a base of all examples in this chapter.
private static 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 dont' want to
implement a full interface. Listener provides a various hooks to
listen tasks execution events.
private static 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.
./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.
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 static enum States { LOCKED, UNLOCKED }
Events.
public static 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-1.0.0.BUILD-SNAPSHOT.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 static enum States { S0, S1, S11, S12, S2, S21, S211, S212 }
Events.
public static 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)); } }
Lets go through what this state machine do when it’s executed and we send various event to it.
sm>sm start Entry state S0 Entry state S1 Entry state S11 Init foo to 0 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 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.
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 use 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 a 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.
Lets 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 static 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 static 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 static enum Variables { CD, TRACK, ELAPSEDTIME } public static 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 static @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 responsibilies 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.
Lets 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 static enum States { READY, FORK, JOIN, CHOICE, TASKS, T1, T1E, T2, T2E, T3, T3E, ERROR, AUTOMATIC, MANUAL }
Events.
public static 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 public TaskExecutor taskExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(5); return taskExecutor; }
Lets 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 static enum States { RUNNING, HISTORY, END, WASHING, RINSING, DRYING, POWEROFF }
Events.
public static 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); }
Lets 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 17, Persist to demonstate 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 19, 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; }
Lets go through a simple example where two different shell instances are
started with command java -jar
spring-statemachine-samples-zookeeper-1.0.0.BUILD-SNAPSHOT.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 20, Showcase to work with a distributed state machine.
Note | |
---|---|
Due to nature of this sample an instanse of a |
Lets go through a simple example where three different sample instances are
started with command java -jar
spring-statemachine-samples-web-1.0.0.BUILD-SNAPSHOT.jar
. 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-1.0.0.BUILD-SNAPSHOT.jar @n2:~# java -jar spring-statemachine-samples-web-1.0.0.BUILD-SNAPSHOT.jar @n3:~# java -jar spring-statemachine-samples-web-1.0.0.BUILD-SNAPSHOT.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 lets press button Event H
and what is supposed to happen is
that internal transition is executed on all state machines changing
extended stare 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 C
which is supposed to take state
machine state back to state S11
and you should see this happening in
all browser sessions.
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.
static enum States { STATE1, STATE2 } static enum Events { EVENT1, EVENT2 }
@Configuration @EnableStateMachine static 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 static class MyBean { @OnTransition(target = "STATE1") void toState1() { } @OnTransition(target = "STATE2") void toState2() { } }
static 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 lets 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.
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.
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 it 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.
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 numbad itself. If you don’t have a full size keyboard you can buy a simple external usb numbad having only numbad 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 statemachines 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.
Note | |
---|---|
This article is not complete as it requires jepsen tests which are planned for next release. |
As mentioned in Chapter 16, Using Distributed States distibuted states are enabled by
wrapper 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 context 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 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 allow use to use predefined set of znodes where current had can be determided with a simple 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 interger is going to overflow thus we don’t need to handle any specific cases.