Open spac-valentin opened 2 years ago
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:
Consider a simple Statemachine with 2 states and 1 triggerless transition:
START -> END
The use case here is:
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
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).
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:
2.5.1
The Statemachine remains in START
.
This is problematic since we cannot leave this state by an event.
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
...
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.
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 froms1
tos2
and then it will transition tos3
ands4
(triggerless transition). I want to make this state machine run froms2
to completion, but it ends up in a infinite loop, executing (and printing)s3 action
What am I missing here?
Note: posted on StackOverflow too