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

Fix infinite loop after resetting state machine #1151

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

radmirr
Copy link

@radmirr radmirr commented Mar 17, 2024

Glossary

Description of unit test state names:
S8_1 - itintial state
S8_2 - custom start state that we pass with default context
S8_5 - end state
S8_3, S8_4 - transit states

Problem description

In some situations (e.g. app crash) we need to start state machine from custom state in order to resume execution. To do that we are following next steps:

  1. Stop state machine with stopReactively;
  2. Reset via accessor with default context and specified state (S8_2);
  3. Start machine with startReactively;

Expected behavior

Transitions from S8_2 to S8_5 with state machine stop

Actual behavior

Infinite loop with transitions S8_2 -> S8_3 -> S8_4 -> S8_5 -> S8_2 -> S8_3 -> ... as decribed in #1011

What causes the problem

  1. Resetting state machine leads to lastState field being filled with custom value that we passed in context (S8_2)
  2. After exiting from last state (S8_5) ReactiveStateMachineExecutor checks triggerless transitions.

  1. In normal scenarios this filter must return false and state machine must stop.

.filter(t -> {
State<S,E> source = t.getSource();
if (source == null) {
return false;
}
State<S,E> currentState = stateMachine.getState();
if (currentState == null) {
return false;
}
if (!StateMachineUtils.containsAtleastOne(source.getIds(), currentState.getIds())) {
return false;
}
if (transitionConflictPolicy != TransitionConflictPolicy.PARENT && completion != null
&& !source.getId().equals(completion.getId())) {
if (source.isOrthogonal()) {
return false;
} else if (!StateMachineUtils.isSubstate(source, completion)) {
return false;
}
}
return true;
})

  1. With not null lastState field we'll get its value instead of currentState because of isComplete() returning true.

    @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;
    }
    }

  2. Comparing S8_2 as source with S8_2 as currentState returns true, causing this check to be skipped and transition t now passing to flatMap. This transition will be called again and again.

    if (!StateMachineUtils.containsAtleastOne(source.getIds(), currentState.getIds())) {
    return false;
    }

Fix description

Main goal of this fix is guaranteed returning of currentState while state machine is in running life cycle.
Triggerless state machines do all work inside STARTING state during startReactively call, so we can count it as a part of running life cycle.

Existing tests was not affected by this change and passed successfully.

Commenting running check from getState will cause added test to run in infinite loop.

@pivotal-cla
Copy link

@radmirr Please sign the Contributor License Agreement!

Click here to manually synchronize the status of this Pull Request.

See the FAQ for frequently asked questions.

@pivotal-cla
Copy link

@radmirr Thank you for signing the Contributor License Agreement!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Resetting spring state machine results in infinite loop
2 participants