spring-projects / spring-statemachine

Spring Statemachine is a framework for application developers to use state machine concepts with Spring.
1.54k stars 604 forks source link

Nested submachine (sub-submachine) with fork and join entry and exit actions ignored or missing context #969

Open adrien-lamoureux opened 3 years ago

adrien-lamoureux commented 3 years ago

Hi there,

I have a statemachine defined with two fork/joins and with each of those submachines having their own fork/join. I have tested that all states correctly transition including all sub states and fork/joins all the way to the final end state. That is great!, however, I discovered two problems related to actions on sub-submachines (2 levels down):

1) Transitioning within a sub-submachine (2 levels down) region never invokes the exit action (but mysteriously, they all invoke their entry actions)

2) Transitioning out of a join from the sub-submachine into another state results in a StateContext containing no useful information on what state is involved (I assume this is because its coming from a join, but I though an entry Action would at least have the target non-null).

Full example showing this issue (will echo entry and exit, as well as a tests on expected states entered/exited):

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.StateMachineBuilder;
import org.springframework.statemachine.config.StateMachineBuilder.Builder;

public class MissingActionExecution {

  private List<String> statesEntered_;
  private List<String> statesExited_;

  public void before()
  {
    statesEntered_ = new ArrayList<String>();
    statesExited_ = new ArrayList<String>();
  }

  public static void main(String[] args) throws Exception {
    MissingActionExecution missingActions = new MissingActionExecution();

    missingActions.before();
    missingActions.run();
  }

  public void run() throws Exception
  {
    Builder<String,String> builder = StateMachineBuilder.builder();

    builder.configureStates().withStates().initial("startState")
      .fork("forkState")
      .state("state2") 
      .join("joinState")
      .state("state3", new EchoEntryAction<String,String>("state3"), new EchoExitAction<String,String>("state3"))
      .end("endState")
      .and()
      .withStates()
         .parent("state2")
         .initial("state21SubStart")
         .state("state21SubStateA", new EchoEntryAction<String,String>("state21SubStateA"), new EchoExitAction<String,String>("state21SubStateA"))
         .fork("forkInState21")
         .state("state21SubStateB")
         .join("joinInState21")
         .state("state21SubStateC", new EchoEntryAction<String,String>("state21SubStateC"), new EchoExitAction<String,String>("state21SubStateC"))
         .end("state21SubEnd")
         .and()
       .withStates()
         .parent("state2")
         .initial("state22SubStart")
         .state("state22SubStateA", new EchoEntryAction<String,String>("state22SubStateA"), new EchoExitAction<String,String>("state22SubStateA"))
         .fork("forkInState22")
         .state("state22SubStateB")
         .join("joinInState22")
         .state("state22SubStateC", new EchoEntryAction<String,String>("state22SubStateC"), new EchoExitAction<String,String>("state22SubStateC"))
         .end("state22SubEnd")
         .and()
         .withStates()
            .parent("state21SubStateB")
            .initial("state21SubStateBStart")
            .state("state21SubStateBA", new EchoEntryAction<String,String>("state21SubStateBA"), new EchoExitAction<String,String>("state21SubStateBA"))
            .state("state21SubStateBB", new EchoEntryAction<String,String>("state21SubStateBB"), new EchoExitAction<String,String>("state21SubStateBB"))
            .state("state21SubStateBC", new EchoEntryAction<String,String>("state21SubStateBC"), new EchoExitAction<String,String>("state21SubStateBC"))
            .end("state21SubStateBEnd")
            .and()
         .withStates()
            .parent("state21SubStateB")
            .initial("state211SubStateBStart")
            .state("state211SubStateBA", new EchoEntryAction<String,String>("state211SubStateBA"), new EchoExitAction<String,String>("state211SubStateBA"))
            .state("state211SubStateBB", new EchoEntryAction<String,String>("state211SubStateBB"), new EchoExitAction<String,String>("state211SubStateBB"))
            .state("state211SubStateBC", new EchoEntryAction<String,String>("state211SubStateBC"), new EchoExitAction<String,String>("state211SubStateBC"))
            .end("state211SubStateBEnd")
          .and()
          .withStates()
             .parent("state22SubStateB")
             .initial("state22SubStateBStart")
             .state("state22SubStateBA", new EchoEntryAction<String,String>("state22SubStateBA"), new EchoExitAction<String,String>("state22SubStateBA"))
             .state("state22SubStateBB", new EchoEntryAction<String,String>("state22SubStateBB"), new EchoExitAction<String,String>("state22SubStateBB"))
             .state("state22SubStateBC", new EchoEntryAction<String,String>("state22SubStateBC"), new EchoExitAction<String,String>("state22SubStateBC"))
             .end("state22SubStateBEnd")
             .and()
          .withStates()
             .parent("state22SubStateB")
             .initial("state222SubStateBStart")
             .state("state222SubStateBA", new EchoEntryAction<String,String>("state222SubStateBA"), new EchoExitAction<String,String>("state222SubStateBA"))
             .state("state222SubStateBB", new EchoEntryAction<String,String>("state222SubStateBB"), new EchoExitAction<String,String>("state222SubStateBB"))
             .state("state222SubStateBC", new EchoEntryAction<String,String>("state222SubStateBC"), new EchoExitAction<String,String>("state222SubStateBC"))
             .end("state222SubStateBEnd");       

    builder.configureTransitions()
    .withExternal().source("startState").target("state2").and()
    .withFork().source("forkState").target("state21SubStart").target("state22SubStart").and()
        .withExternal().source("state21SubStart").target("state21SubStateA").and()
        .withExternal().source("state21SubStateA").target("state21SubStateB").and()
        .withFork().source("forkInState21").target("state21SubStateBStart").target("state211SubStateBStart").and()
          .withExternal().source("state21SubStateBStart").target("state21SubStateBA").and()
          .withExternal().source("state21SubStateBA").target("state21SubStateBB").and()
          .withExternal().source("state21SubStateBB").target("state21SubStateBC").and()
          .withExternal().source("state21SubStateBC").target("state21SubStateBEnd").and()
          .withExternal().source("state211SubStateBStart").target("state211SubStateBA").and()
          .withExternal().source("state211SubStateBA").target("state211SubStateBB").and()
          .withExternal().source("state211SubStateBB").target("state211SubStateBC").and()
          .withExternal().source("state211SubStateBC").target("state211SubStateBEnd").and()
        .withJoin().source("state21SubStateBEnd").source("state211SubStateBEnd").target("joinInState21").and()            
        .withExternal().source("joinInState21").target("state21SubStateC").and()
        .withExternal().source("state21SubStateC").target("state21SubEnd").and()
        .withExternal().source("state22SubStart").target("state22SubStateA").and()
        .withExternal().source("state22SubStateA").target("state22SubStateB").and()
        .withFork().source("forkInState22").target("state22SubStateBStart").target("state222SubStateBStart").and()
          .withExternal().source("state22SubStateBStart").target("state22SubStateBA").and()
          .withExternal().source("state22SubStateBA").target("state22SubStateBB").and()
          .withExternal().source("state22SubStateBB").target("state22SubStateBC").and()
          .withExternal().source("state22SubStateBC").target("state22SubStateBEnd").and()
          .withExternal().source("state222SubStateBStart").target("state222SubStateBA").and()
          .withExternal().source("state222SubStateBA").target("state222SubStateBB").and()
          .withExternal().source("state222SubStateBB").target("state222SubStateBC").and()
          .withExternal().source("state222SubStateBC").target("state222SubStateBEnd").and()              
        .withJoin().source("state22SubStateBEnd").source("state222SubStateBEnd").target("joinInState22").and()
        .withExternal().source("joinInState22").target("state22SubStateC").and()
        .withExternal().source("state22SubStateC").target("state22SubEnd").and()
   .withJoin().source("state21SubEnd").source("state22SubEnd").target("joinState").and()
   .withExternal().source("joinState").target("state3").and()
   .withExternal().source("state3").target("endState");

    StateMachine<String,String> sm = builder.build();

    sm.start();

    System.out.println("End State is:" +sm.getState().getIds() + "\n");

    Set<String> expected = new HashSet<String>(Arrays.asList(new String[]{
        "state21SubStateA",
        "state21SubStateBA","state21SubStateBB","state21SubStateBC",
        "state211SubStateBA","state211SubStateBB","state211SubStateBC",
        "state21SubStateC",
        "state22SubStateA",
        "state22SubStateBA","state22SubStateBB","state22SubStateBC",
        "state222SubStateBA","state222SubStateBB","state222SubStateBC",
        "state22SubStateC",
        "state3"}));

    boolean statesExpectedOnEntry = new HashSet<String>(statesEntered_).equals(expected);
    if (!statesExpectedOnEntry)
    {
        System.out.println("ENTRY TEST FAIL: \nfound:\n" + statesEntered_ + "\nexpected: \n" + expected + "\n");
    }

    boolean statesExpectedOnExit = new HashSet<String>(statesExited_).equals(expected);
    if (!statesExpectedOnExit)
    {
        System.out.println("EXIT TEST FAIL: \nfound:\n" + statesExited_ + "\nexpected: \n" + expected + "\n");
    }    

    sm.stop();    
  }

  private class EchoEntryAction<S, E> implements Action<S, E> {

    private String state_;
    public EchoEntryAction(String state)
    {
      state_= state;
    }    

    public void execute(StateContext<S, E> context) {

      if (context == null || context.getTarget() == null)
      {
        System.out.println("Entered NULL TARGET: " + state_);
        //statesEntered_.add(state_);
        return;
      }

      statesEntered_.add((String)context.getTarget().getId());
      System.out.println("Entered: " + context.getTarget().getId());

    }

  }

  private class EchoExitAction<S, E> implements Action<S, E> {

    private String state_;
    public EchoExitAction(String state)
    {
      state_= state;
    }

    public void execute(StateContext<S, E> context) {

      if (context == null || context.getSource() == null)
      {
        System.out.println("Exited NULL SOURCE: " + state_);
        //statesExited_.add(state_);
        return;
      }      

      statesExited_.add((String)context.getSource().getId());
      System.out.println("Exited: " + context.getSource().getId());

    }
  }

}