Fork and Join in spring state machine (Parallel execution) - spring

I have an UML diagram as below :
How to configure states and transitions in state machine from SI to END based on events....
public class Config14
extends EnumStateMachineConfigurerAdapter<States, Events> {
#Override
public void configure(StateMachineStateConfigurer<States, Events> states)
throws Exception {
.....
}
#Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions)
throws Exception {
.....
}
}

Though it is late to answer, I will put my findings anyways.
To achieve the show state transitions, you need to have a fork/join pseudo states. As it has been mentioned over here (https://www.uml-diagrams.org/state-machine-diagrams.html), Pseudostates are typically used to connect multiple transitions into more complex state transitions paths. And their respective definition is as follows:
Fork pseudostate vertices serve to split an incoming transition into two or more transitions terminating on orthogonal target vertices (i.e., vertices in different regions of a composite state).
Join pseudostate merges several transitions originating from source vertices in different orthogonal regions.
One should note that segments outgoing from a fork vertex must not have guards or triggers(same applies for a Join).
Your config should more or less be like the following:
#Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial("SI")
.fork("S1")
.join("JOIN")
.state("S11")
.state("S12")
.state("S2")
.end("END")
.and()
.withStates()
.parent("S1")
.initial("S11")
.end("S11") //not very sure about this
.and()
.withStates()
.parent("S1")
.initial("S12")
.end("S12");
}
and
#Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions.withExternal()
.source("SI").target("S1").event("YOUR_EVENT")
.and()
.withExternal()
.source("S2").target("END").event("YOUR_EVENT")
.and()
.withFork()
.source("S1")
.target("S11")
.target("S12")
.and()
.withJoin()
.source("S11")
.source("S12")
.target("JOIN");
}
For a sample, check https://github.com/amenski/statemachine

Related

Spring state machine configuration for rest service requests

I have a simple order processing application and trying to apply spring state machine for handling states of the order.
And I wondering how can I handle order's states for the same order during multiple request from rest service.
Order states:
enum OrderEvents {
FULFILL,
PAY,
CANCEL
}
Order events:
enum OrderStates {
SUBMITTED,
PAID,
FULFILLED,
CANCELLED
}
State machine configuration:
#Log
#Configuration
#EnableStateMachineFactory
class SimpleEnumStatemachineConfiguration extends StateMachineConfigurerAdapter<OrderStates, OrderEvents> {
#Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
transitions
.withExternal().source(OrderStates.SUBMITTED).target(OrderStates.PAID).event(OrderEvents.PAY)
.and()
.withExternal().source(OrderStates.PAID).target(OrderStates.FULFILLED).event(OrderEvents.FULFILL)
.and()
.withExternal().source(OrderStates.SUBMITTED).target(OrderStates.CANCELLED).event(OrderEvents.CANCEL)
.and()
.withExternal().source(OrderStates.PAID).target(OrderStates.CANCELLED).event(OrderEvents.CANCEL);
}
#Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(OrderStates.SUBMITTED)
.state(OrderStates.PAID)
.end(OrderStates.FULFILLED)
.end(OrderStates.CANCELLED);
}
#Override
public void configure(StateMachineConfigurationConfigurer<OrderStates, OrderEvents> config) throws Exception {
config.withConfiguration()
.autoStartup(true)
}
}
In my order service I call
StateMachine<OrderStates, OrderEvents> sm = this.factory.getStateMachine(orderIdKey);
But it seems that every time is new state machine created even for the same orderIdKey. So, how can I get access to state machine created when order was submitted on next state?
You have basically two options:
a) persist the state machine for the given orderId, using a state machine persister as explained here.
b) create a new state machine for given orderId (per HTTP request) and rehydrate the SM state based on the state of the order entity for the given orderId. SM objects are considered lightweight, so this is a viable approach as well. Below is a code sample:
StateMachine<Status, Event> build(long orderId) {
orderService.getOrder(orderId) //returns Optional
.map(order -> {
StateMachine<Status, Event> sm = stateMachineFactory.getStateMachine(Long.toString(orderId));
sm.stop();
rehydrateState(sm, sm.getExtendedState, order.getStatus());
sm.start();
return sm;
})
.orElseGet(() -> createNewStateMachine(orderId);
}
void rehydrateState(StateMachine<Status, Event> newStateMachine, ExtendedState extendedState, Status orderStatus) {
newStateMachine.getStateMachineAccessor().doWithAllRegions(sma ->
sma.resetStateMachine(new DefaultStateMachineContext<>(orderStatus, null, null, extendedState));
});
}

Spring state-machine: common transition

Is there a way to configure common external transition for multiple events?
i.e.
For all states X,Y,Z -> CANCELED on CANCEL event.
Where X,Y,Z - sources, CANCELED - target state
For CANCELED state run common internal transition on all events
I hame more than 20 states to apply this transition to and it is not convenient to manually configure all of them.
I am using EnumStateMachineConfigurerAdapter and #EnableStateMachineFactory to configure state machine, my configuration looks like this
#Configuration
#EnableStateMachineFactory
public class ContentOnlyStateMachineConfig extends EnumStateMachineConfigurerAdapter<TaskState, WorkflowEvent> {
private final Action<TaskState, WorkflowEvent> someAction;
private final Action<TaskState, WorkflowEvent> anotherAction;
#Autowired
public ContentOnlyStateMachineConfig(
final Action<TaskState, WorkflowEvent> someAction,
final Action<TaskState, WorkflowEvent> anotherAction) {
this.someAction = someAction;
this.anotherAction = anotherAction;
}
#Override
public void configure(final StateMachineStateConfigurer<TaskState, WorkflowEvent> states)
throws Exception {
states
.withStates()
.initial(TaskState.CONTENT)
.end(TaskState.COMPLETE)
.state(TaskState.CONTENT, someAction)
.state(TaskState.COMPLETE, anotherAction);
}
#Override
public void configure(
final StateMachineTransitionConfigurer<TaskState, WorkflowEvent> transitions)
throws Exception {
transitions
.withExternal()
.source(TaskState.CONTENT).target(TaskState.SOME_STATE)
.event(WorkflowEvent.EXCEPTION)
.and()
.withExternal()
.source(TaskState.EXCEPTION).target(TaskState.CANCELED)
.event(WorkflowEvent.CANCEL)
.and()
.withExternal()
.source(TaskState.SOME_STATE_2).target(TaskState.SOME_STATE_1)
.event(WorkflowEvent.SOME_EVENT_3)
.and()
.withExternal()
.source(TaskState.SOME_STATE).target(TaskState.SOME_STATE_2)
.event(WorkflowEvent.SOME_EVENT_2)
.and()
.withExternal()
.source(TaskState.SOME_STATE).target(TaskState.COMPLETE)
.event(WorkflowEvent.ADMIN_ACCEPT)
.and()
.withExternal()
.source(TaskState.SOME_STATE).target(TaskState.CONTENT)
.event(WorkflowEvent.SOME_EVENT);
}
}
There's not shortcuts to allow user to create a spaghetti ;). Having said that, if your statechart after you have drawn it in a paper looks like spaghetti rather that clear statechart, I'd argue you're doing something wrong.
Keeping everything in a one flat machine easily creates this kind of state explosion whey configuration itself is starting to look difficult to understand. Usually this is a moment when machine designer needs to start thinking about use of nested substates. If you have multiple states where same event should take machine to same state, you already have a shared functionality in those states.
Putting those states into substates would then allow one single transition from their parent state to transit away. This is usually a model how things are done in machines.
It's not supported out-of-the-box a way to configure a transition with multiple sources and a single target.
Need to do this explicitly.
For example:
#Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception {
transitions.withExternal()
// normal transitions configuration ..
// common error transitions
for (var state : EnumSet.of(States.A, States,B, States.C)) {
transitions.withExternal().source(state).target(States.ERROR).event(ERROR_EVENT);
}
}
(Can't use Stream.of() since withExternal() throws a checked exception..)

Spring Batch Processor Exception Listener?

I have a partitioned Spring Batch job that reads several split up CSV files and processes each in their own thread, then writes the results to a corresponding output file.
If an item fails to process though (an exception is thrown), I want to write that result to an error file. Is there a way to add a writer or listener that can handle this?
Taking this one step further, is there a way to split this up by exception type and write the different exceptions to different files?
You can achieve this by specifying SkipPolicy. Implement this interface and add your own logic.
public class MySkipper implements SkipPolicy {
#Override
public boolean shouldSkip(Throwable exception, int skipCount) throws SkipLimitExceededException {
if (exception instanceof XYZException) {
//doSomething
}
......
}
You can specify this skip policy in your batch.
this.stepBuilders.get("importStep").<X, Y>chunk(10)
.reader(this.getItemReader()).faultTolerant().skipPolicy(....)
.processor(this.getItemProcessor())
.writer(this.getItemWriter())
.build();
One way that I have seen this done is through a combination of a SkipPolicy and a SkipListener.
The policy would allow you to skip over items that threw an exception, such as a FlatFileParseException (skippable exceptions can be configured).
The listener gives you access to the Throwable and the item that caused it (or just Throwable in the case of reads). The skip listener also lets you differentiate between skips in the read/processor/writer if you wanted to handle those separately.
public class ErrorWritingSkipListener<T, S> implements SkipListener<T, S> {
#Override
public void onSkipInRead(final Throwable t) {
// custom logic
}
#Override
public void onSkipInProcess(final T itemThatFailed, final Throwable t) {
// custom logic
}
#Override
public void onSkipInWrite(final S itemThatFailed, final Throwable t) {
// custom logic
}
}
I would recommend using the SkipPolicy only to identify the exceptions you want to write out to your various files, and leveraging the SkipListener to perform the actual file writing logic. That would match up nicely with their intended use as defined by their interfaces.

StateMachineInterceptor fired on transient states

I have a transition from A -> X_choice, and X_choice will go to either B or C. I would like to have an interceptor so i can log transitions from A -> B || C. When i try to hook into any of the possible events, (preStateChanged, etc), I only get the transition going from A -> X_choice. The callbacks don't get fired when going from X_choice -> B or X_choice. Is there any way for me to easily do this?
#Override
public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception {
states.withStates()
.initial(States.A)
.junction(States.X_choice)
.state(States.B)
.state(States.C)
#Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception {
transition
.withExternal().source(States.A).target(States.X_choice).event(Events.E).and()
.withJunction()
.source(States.X_choice)
.first(States.B, guard())
.last(States.C).and()
And I have a StateMachinerInteceptor defined as follows:
private class PersistingStateChangeInterceptor extends StateMachineInterceptorAdapter<States, Events> {
#Override
public void preStateChange(State<AppCfgBreatheAgain.States, AppCfgBreatheAgain.Events> state, Message<AppCfgBreatheAgain.Events> message, Transition<AppCfgBreatheAgain.States, AppCfgBreatheAgain.Events> transition, StateMachine<AppCfgBreatheAgain.States, AppCfgBreatheAgain.Events> stateMachine) {
transition.getSource(); //A
transition.getTarget(); //X_Choice
}
What I want is this:
transition.getSource(); //A
transition.getTarget(); //B
Or alternatively two callbacks, one going from A -> X_choice, and one from X_choice -> B. Is this feasible? I need this as I am using the Persist recipe, and the persist is actually persisting the Choice state instead of the actual terminal state.
Developer resolved this issue in this related github ticket:
https://github.com/spring-projects/spring-statemachine/issues/266

spring state machine changing to one of two possible states based on action

i'm having trouble configuring my SSM for a very simple scenario:
the culprit is the 'validation' action that needs to happen when the log_on event is fired. i re-read the SSM documentation and am not sure if this is a 'fork', 'guard' or a hierarchical configuration.
the 'action' is performed here:
#Component
public class LogonValidationAction implements Action<PickStates, PickEvents> {
Logger logger = LogManager.getLogger();
volatile int counter = 0;
#Override
public void execute(StateContext<PickStates, PickEvents> context) {
String eventName = context.getEvent().name();
logger.info("executing {} for event {}", LogonValidationAction.class.getName(), eventName);
// basically, if success, send success, if failure, send failure
// for testing
if(counter % 2 == 0)
context.getStateMachine().sendEvent(PickEvents.logon_sucess);
else
context.getStateMachine().sendEvent(PickEvents.logon_fail);
}
}
i am using 'counter' in my test to direct the flow to either 'failure' (logged_off state remains) or 'success' (state changes to logged_on).
here's the configuration:
#Configuration
#EnableStateMachineFactory
public class Config extends EnumStateMachineConfigurerAdapter<PickStates, PickEvents>{
#Autowired
StateMachineListener stateMachineListener;
#Autowired
LogonValidationAction logonValidationAction;
#Override
public void configure(StateMachineStateConfigurer<PickStates, PickEvents> states) throws Exception {
states
.withStates()
.initial(PickStates.logged_off)
.state(PickStates.logged_on)
.states(EnumSet.allOf(PickStates.class));
}
#Override
public void configure(StateMachineTransitionConfigurer<PickStates, PickEvents> transitions) throws Exception {
transitions
.withExternal()
.source(PickStates.logged_off)
.target(PickStates.logged_on)
.event(PickEvents.log_on)
.action(logonValidationAction)
.and()
.withExternal()
.source(PickStates.logged_on)
.target(PickStates.logged_off)
.event(PickEvents.log_off);
}
#Override
public void configure(StateMachineConfigurationConfigurer<PickStates, PickEvents> config) throws Exception {
config
.withConfiguration()
.autoStartup(true)
.listener(stateMachineListener);
}
}
this is the failing test (expecting 'logged_off' when the action fires the logon_fail event):
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = Application.class)
public class EzpickStateMachineTest {
Logger logger = LogManager.getLogger();
#Autowired
StateMachineFactory<PickStates, PickEvents> stateMachineFactory;
// for testing so can toggle success/failure when logging in
#Autowired
LogonValidationAction logonValidationAction;
#Test
public void failLogon() throws Exception {
StateMachine<PickStates, PickEvents> stateMachine = stateMachineFactory.getStateMachine();
stateMachine.start();
// when sm starts, state is 'logged_off'
assertThat(stateMachine.getState().getId().name(), is(PickStates.logged_off.name()));
// odd numbers fire 'failure' event
logonValidationAction.setCounter(1);
stateMachine.sendEvent(PickEvents.log_on);
// if logon fails, state is 'logged_off'
assertThat(stateMachine.getState().getId().name(), is(PickStates.logged_off.name()));
}
}
From action you try to send either logon_sucess or logon_fail but in machine configuration you haven't defined any transitions for those events so machine would not do anything.
Other notes, don't store counter in action bean, use machine extended state variables for it as then it becomes visible to every component using machine or StateContext.
When that counter is available in extended state, you can i.e. create an anonymous transition with a guard which uses that counter from extended state. One other possibility is to use choice pseudostate and guards to make a choice between target states. Your use of counter is exactly what wiki mention about in its extended state section https://en.wikipedia.org/wiki/UML_state_machine#Extended_states

Resources