spring-statemachine
spring-statemachine copied to clipboard
Resetting spring state machine results in infinite loop
I am exploring Spring state machine and was testing a scenario where state machine would run to completion after resetting from a specific state but it runs in an infinite loop. The below code is enough to reproduce the issue.
Basically, there is one state machine that on event e1
will transition from s1
to s2
and then it will transition to s3
and s4
(triggerless transition). I want to make this state machine run from s2
to completion, but it ends up in a infinite loop, executing (and printing) s3 action
@Test
public void loop() throws Exception {
var sm = buildMachine();
Flux.fromIterable(sm.getStateMachineAccessor().withAllRegions())
.flatMap(region -> region.resetStateMachineReactively(new DefaultStateMachineContext<>("s2", null, null, null)))
.subscribe();
sm.startReactively().block();
sm.sendEvent(Mono.just(MessageBuilder
.withPayload("e1")
.build())).subscribe();
}
@SneakyThrows
StateMachine<String, String> buildMachine() {
StateMachineBuilder.Builder<String, String> builder = StateMachineBuilder.builder();
builder.configureConfiguration()
.withConfiguration()
.autoStartup(false);
builder.configureStates()
.withStates()
.initial("s1")
.end("s4")
.states(Set.of("s1", "s2", "s3", "s4"));
// @formatter:off
builder.configureTransitions()
.withExternal()
.source("s1")
.event("e1")
.target("s2")
.action((p) -> logger.info("s2 action"), (p) -> logger.info("s2 action failed"))
.and()
.withExternal()
.source("s2")
.target("s3")
.action((p) -> logger.info("s3 action"))
.and()
.withExternal()
.source("s3")
.target("s4");
// @formatter:on
return builder.build();
}
What am I missing here?
Note: posted on StackOverflow too
My gut says that starting the machine when resetting is the culprit: https://github.com/spring-projects/spring-statemachine/blob/af4cfc4d70212503ed1b066c859f2c59b387d78a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java#L887-L890
Added a listener for more info and for some reason, at some point, state changes from. s4
to s3
(it goes backwards)
2021-11-21 14:20:05.324 INFO 83251 --- [ Test worker] com.rezz.sm.TestSM : state changed from s3 to s4
2021-11-21 14:20:05.324 INFO 83251 --- [ Test worker] com.rezz.sm.TestSM : stateContext DefaultStateContext [stage=STATE_CHANGED, message=null, messageHeaders={id=ec7792f5-d444-f2ff-b30a-8104e79b3613, timestamp=1637497205324}, extendedState=DefaultExtendedState [variables={}], transition=null, stateMachine=s2 s1 s4 s3 / s4 / uuid=d52c0022-1282-42da-9005-77a0d90b29a3 / id=null, source=ObjectState [getIds()=[s3], getClass()=class org.springframework.statemachine.state.ObjectState, hashCode()=220055230, toString()=AbstractState [id=s3, pseudoState=null, deferred=[], entryActions=[], exitActions=[], stateActions=[], regions=[], submachine=null]], target=ObjectState [getIds()=[s4], getClass()=class org.springframework.statemachine.state.ObjectState, hashCode()=41376554, toString()=AbstractState [id=s4, pseudoState=org.springframework.statemachine.state.DefaultPseudoState@651df273, deferred=[], entryActions=[], exitActions=[], stateActions=[], regions=[], submachine=null]], sources=null, targets=null, exception=null]
2021-11-21 14:20:05.325 INFO 83251 --- [ Test worker] com.rezz.sm.TestSM : transitionEnded from s3 to s4
2021-11-21 14:20:05.325 INFO 83251 --- [ Test worker] com.rezz.sm.TestSM : stateContext DefaultStateContext [stage=TRANSITION_END, message=null, messageHeaders={id=daef3be2-2282-923f-c11d-70950629f8f8, timestamp=1637497205325}, extendedState=DefaultExtendedState [variables={}], transition=AbstractTransition [source=ObjectState [getIds()=[s3], getClass()=class org.springframework.statemachine.state.ObjectState, hashCode()=220055230, toString()=AbstractState [id=s3, pseudoState=null, deferred=[], entryActions=[], exitActions=[], stateActions=[], regions=[], submachine=null]], target=ObjectState [getIds()=[s4], getClass()=class org.springframework.statemachine.state.ObjectState, hashCode()=41376554, toString()=AbstractState [id=s4, pseudoState=org.springframework.statemachine.state.DefaultPseudoState@651df273, deferred=[], entryActions=[], exitActions=[], stateActions=[], regions=[], submachine=null]], kind=EXTERNAL, guard=null], stateMachine=s2 s1 s4 s3 / s4 / uuid=d52c0022-1282-42da-9005-77a0d90b29a3 / id=null, source=null, target=null, sources=null, targets=null, exception=null]
2021-11-21 14:20:05.325 INFO 83251 --- [ Test worker] com.rezz.sm.TestSM : state changed from s4 to s3
2021-11-21 14:20:05.325 INFO 83251 --- [ Test worker] com.rezz.sm.TestSM : stateContext DefaultStateContext [stage=STATE_CHANGED, message=null, messageHeaders={id=684accd0-fd1f-0c9a-c67c-bc3c1b0ad2e2, timestamp=1637497205325}, extendedState=DefaultExtendedState [variables={}], transition=null, stateMachine=s2 s1 s4 s3 / s4 / uuid=d52c0022-1282-42da-9005-77a0d90b29a3 / id=null, source=ObjectState [getIds()=[s4], getClass()=class org.springframework.statemachine.state.ObjectState, hashCode()=41376554, toString()=AbstractState [id=s4, pseudoState=org.springframework.statemachine.state.DefaultPseudoState@651df273, deferred=[], entryActions=[], exitActions=[], stateActions=[], regions=[], submachine=null]], target=ObjectState [getIds()=[s3], getClass()=class org.springframework.statemachine.state.ObjectState, hashCode()=220055230, toString()=AbstractState [id=s3, pseudoState=null, deferred=[], entryActions=[], exitActions=[], stateActions=[], regions=[], submachine=null]], sources=null, targets=null, exception=null]
2021-11-21 14:20:05.325 INFO 83251 --- [ Test worker] com.rezz.sm.TestSM : transitionEnded from s2 to s3
My gut says that starting the machine when resetting is the culprit:
I would agree that this is at least a use case that exposes the issue. I encountered the same behavior when using a StateMachinePersister
. Here is a description of my issue that fits the one from above:
Issue Setting
Consider a simple Statemachine with 2 states and 1 triggerless transition:
START -> END
The use case here is:
- start the machine that has been 1.1 newly created 1.2 restored
Note that use case point 1.1 is meant for reference. The point of interest is 1.2 where the Statemachine is started as part of the persister's restore procedure: https://github.com/spring-projects/spring-statemachine/blob/cf4eb2f7c0bc861bbd1013da6332e2de4188c8cb/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractStateMachinePersister.java#L66-L73
Version Specifics
The issue mainly concerns Spring Statemachine Core 3.2.0
but points out something that might be wrong as well with version 2.5.1
(before going reactive).
Issue Description
The method StateMachine#start( )
behaves differently in version 2.5.1
and 3.2.0
.
I attached a MWE with a test for reference (built with Gradle 7.5.1
and JDK 17.0.6
).
There are 2 test cases, one for each point from the use case as mentioned above. Both Statemachine versions succeed with point 1.1 but fail 1.2:
Version 2.5.1
The Statemachine remains in START
.
This is problematic since we cannot leave this state by an event.
Version 3.2.0
The Statemachine moves to END
but does not complete. Instead, it hangs / loops infinitely:
o.s.s.s.ReactiveStateMachineExecutor - About to handleTriggerlessTransitions
o.s.s.support.LifecycleObjectSupport - stopReactively END START / END / uuid=30cdaa9c-4736-47d1-be70-e7ad890b919b / id=null with rlm [lifecyclestate=STARTING, owner=END START / END / uuid=30cdaa9c-4736-47d1-be70-e7ad890b919b / id=null]
o.s.s.s.ReactiveLifecycleManager - Request stopReactively [lifecyclestate=STARTING, owner=END START / END / uuid=30cdaa9c-4736-47d1-be70-e7ad890b919b / id=null]
o.s.s.s.ReactiveLifecycleManager - Don't own, requesting to postpone stop[lifecyclestate=STARTING, owner=END START / END / uuid=30cdaa9c-4736-47d1-be70-e7ad890b919b / id=null]
o.s.s.support.AbstractStateMachine - Trying Exit state=[ObjectState [getIds()=[END], getClass()=class org.springframework.statemachine.state.ObjectState, hashCode()=738937987, toString()=AbstractState [id=END, pseudoState=org.springframework.statemachine.state.DefaultPseudoState@78525ef9, deferred=[], entryActions=[], exitActions=[], stateActions=[], regions=[], submachine=null]]]
o.s.s.support.AbstractStateMachine - Exit state=[ObjectState [getIds()=[END], getClass()=class org.springframework.statemachine.state.ObjectState, hashCode()=738937987, toString()=AbstractState [id=END, pseudoState=org.springframework.statemachine.state.DefaultPseudoState@78525ef9, deferred=[], entryActions=[], exitActions=[], stateActions=[], regions=[], submachine=null]]]
o.s.s.support.AbstractStateMachine - Trying Enter state=[ObjectState [getIds()=[END], getClass()=class org.springframework.statemachine.state.ObjectState, hashCode()=738937987, toString()=AbstractState [id=END, pseudoState=org.springframework.statemachine.state.DefaultPseudoState@78525ef9, deferred=[], entryActions=[], exitActions=[], stateActions=[], regions=[], submachine=null]]]
o.s.s.support.AbstractStateMachine - Trying Enter state=[ObjectState [getIds()=[END], getClass()=class org.springframework.statemachine.state.ObjectState, hashCode()=738937987, toString()=AbstractState [id=END, pseudoState=org.springframework.statemachine.state.DefaultPseudoState@78525ef9, deferred=[], entryActions=[], exitActions=[], stateActions=[], regions=[], submachine=null]]]
o.s.s.support.AbstractStateMachine - Enter state=[ObjectState [getIds()=[END], getClass()=class org.springframework.statemachine.state.ObjectState, hashCode()=738937987, toString()=AbstractState [id=END, pseudoState=org.springframework.statemachine.state.DefaultPseudoState@78525ef9, deferred=[], entryActions=[], exitActions=[], stateActions=[], regions=[], submachine=null]]]
o.s.s.support.AbstractStateMachine - State onComplete: state=[ObjectState [getIds()=[END], getClass()=class org.springframework.statemachine.state.ObjectState, hashCode()=738937987, toString()=AbstractState [id=END, pseudoState=org.springframework.statemachine.state.DefaultPseudoState@78525ef9, deferred=[], entryActions=[], exitActions=[], stateActions=[], regions=[], submachine=null]]] context=[DefaultStateContext [stage=STATE_ENTRY, message=null, messageHeaders={id=c8c9f2be-113e-04f0-724b-1f9e14598be2, timestamp=1682614107675}, extendedState=DefaultExtendedState [variables={}], transition=AbstractTransition [source=ObjectState [getIds()=[START], getClass()=class org.springframework.statemachine.state.ObjectState, hashCode()=755944228, toString()=AbstractState [id=START, pseudoState=org.springframework.statemachine.state.DefaultPseudoState@4d654825, deferred=[], entryActions=[], exitActions=[], stateActions=[], regions=[], submachine=null]], target=ObjectState [getIds()=[END], getClass()=class org.springframework.statemachine.state.ObjectState, hashCode()=738937987, toString()=AbstractState [id=END, pseudoState=org.springframework.statemachine.state.DefaultPseudoState@78525ef9, deferred=[], entryActions=[], exitActions=[], stateActions=[], regions=[], submachine=null]], kind=EXTERNAL, guard=null], stateMachine=END START / END / uuid=30cdaa9c-4736-47d1-be70-e7ad890b919b / id=null, source=null, target=null, sources=null, targets=null, exception=null]]
o.s.s.s.ReactiveStateMachineExecutor - About to handleTriggerlessTransitions
...
Expected Behavior
Starting the Statemachine should always execute the triggerless transition to END
.
Version 4.0.0
, problem still persist.
Another fact about problem: setting lastState
field of stateMachine to null (via debugger or reflection) between restore and startReactively call leads to correct execution without loop.
This field affects behavior of this method
https://github.com/spring-projects/spring-statemachine/blob/4495c7dd7784f35d78c6e88c490466e05bc24cb9/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java#L184-L194
And looks like on some point (while performing stopReactively) machine returning lastState instead of current state and machine goes back to infinite loop and never finish.