Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resetting spring state machine results in infinite loop #1011

Open
spac-valentin opened this issue Nov 21, 2021 · 4 comments · May be fixed by #1151
Open

Resetting spring state machine results in infinite loop #1011

spac-valentin opened this issue Nov 21, 2021 · 4 comments · May be fixed by #1151
Labels
status/need-triage Team needs to triage and take a first look

Comments

@spac-valentin
Copy link

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

@github-actions github-actions bot added the status/need-triage Team needs to triage and take a first look label Nov 21, 2021
@spac-valentin
Copy link
Author

My gut says that starting the machine when resetting is the culprit:

.thenEmpty(Mono.defer(() -> {
if (currentState instanceof StateMachineReactiveLifecycle) {
return ((StateMachineReactiveLifecycle) currentState).startReactively();
}

@spac-valentin
Copy link
Author

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

@mroeppis
Copy link

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:

  1. 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:

@Override
public final StateMachine<S, E> restore(StateMachine<S, E> stateMachine, T contextObj) throws Exception {
final StateMachineContext<S, E> context = stateMachinePersist.read(contextObj);
stateMachine.stopReactively().block();
stateMachine.getStateMachineAccessor().doWithAllRegions(function -> function.resetStateMachineReactively(context).block());
stateMachine.startReactively().block();
return stateMachine;
}

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.

@radmirr
Copy link

radmirr commented Mar 16, 2024

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

@Override
public State<S,E> getState() {
// if we're complete assume we're stopped
// and state was stashed into lastState
State<S, E> s = lastState;
if (s != null && isComplete()) {
return s;
} else {
return currentState;
}
}

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.

radmirr added a commit to radmirr/spring-statemachine that referenced this issue Mar 17, 2024
@radmirr radmirr linked a pull request Mar 17, 2024 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status/need-triage Team needs to triage and take a first look
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants