springstate-machinespring-statemachine

Spring statemachine Persist recipe with two state machine configurations


I am trying to extend the Persist sample of spring statemachine to two different state machine configurations. http://docs.spring.io/spring-statemachine/docs/1.0.0.RELEASE/reference/htmlsingle/#statemachine-examples-persist

Therefor I

No big deal so far. Now to the Configuration:

Furthermore I adapted the AbstractStateMachineCommands class and autowire a list of statemachines in there. The start/stop and state methods now start/stop and print state of every state machine (here I do not care about print, variables).

The occuring problem is:

So, can anybody tell me where I messed it up? Or is the Persist recipe not applicable to two state machines?

Thanks a lot.

Code Examples: The Application.java now contains these configs and entities:

    @Configuration
static class PersistHandlerConfig {

    @Bean
    public Persist persist() throws Exception {
        return new Persist(persistStateMachineHandler());
    }

    @Bean
    public PersistStateMachineHandler persistStateMachineHandler() throws Exception {
        return new PersistStateMachineHandler(persistSm());
    }

    @Bean
    public StateMachine<String, String> persistSm() throws Exception{

        Builder<String, String> builder = StateMachineBuilder.builder();
        builder.configureStates()
            .withStates()
                .initial("PLACED")
                .state("PROCESSING")
                .state("SENT")
                .state("DELIVERED");

        builder.configureTransitions()
            .withExternal()
                .source("PLACED").target("PROCESSING")
                .event("PROCESS")
                .and()
            .withExternal()
                .source("PROCESSING").target("SENT")
                .event("SEND")
                .and()
            .withExternal()
                .source("SENT").target("DELIVERED")
                .event("DELIVER");

        return builder.build();
    }
}

@Configuration
static class TicketPersistHandlerConfig {

  @Bean
  public TicketPersist ticketPersist() throws Exception {
    return new TicketPersist(ticketPersistStateMachineHandler());
  }

  @Bean
  public PersistStateMachineHandler ticketPersistStateMachineHandler() throws Exception {
    return new PersistStateMachineHandler(buildMachine());
  }

  @Bean
  public StateMachine<String, String> buildMachine() throws Exception {

       Builder<String, String> builder = StateMachineBuilder.builder();
       builder.configureStates()
           .withStates()
               .initial("PRINTED")
               .state("BOOKED")
               .state("SOLD")
               .state("DELIVERED");

       builder.configureTransitions()
           .withExternal()
               .source("PRINTED").target("BOOKED")
               .event("BOOK")
               .and()
           .withExternal()
               .source("BOOKED").target("SOLD")
               .event("SELL")
               .and()
           .withExternal()
               .source("SOLD").target("DELIVERED")
               .event("DELIVER");

       return builder.build();
   }

}

public static class Order {
    int id;
    String state;

    public Order(int id, String state) {
        this.id = id;
        this.state = state;
    }

    @Override
    public String toString() {
        return "Order [id=" + id + ", state=" + state + "]";
    }

}

public static class Ticket {
  int id;
  String state;

  public Ticket(int id, String state) {
    this.id = id;
    this.state = state;
  }

  @Override
  public String toString() {
    return "Ticket [id=" + id + ", state=" + state + "]";
  }

}

TicketPersist.java and TicketPersistCommands.java are the same like the ones for orders (just replaced order(s) with ticket(s)). I adapted AbstractStateMachineCommands in the following way:

@Autowired
private List<StateMachine<S, E>> stateMachines;
@CliCommand(value = "sm start", help = "Start a state machine")
public String start() {
  for (StateMachine<S, E> stateMachine : stateMachines)
  {
    stateMachine.start();
  }
    return "State machines started";
}

@CliCommand(value = "sm stop", help = "Stop a state machine")
public String stop() {
  for (StateMachine<S, E> stateMachine : stateMachines)
  {
    stateMachine.stop();
  }
    return "State machines stopped";
}

Solution

  • There is a conceptual difference between plain annotation configuration(use of @EnableStateMachine and adapter) and manual builder. Latter is really meant to be used outside of spring app context and while you can then register machine created from it as bean(like you tried to do) a lot of automatic configuration is not applied. I'll probably need to pay more attention of this use case in test(where user returns machine from builder registered as @Bean).

    1. If you get NPE when two machines are created with @EnableStateMachine, that's a bug I need to look into. You should use name field with @EnableStateMachine indicating a bean name adapter/javaconfig would use if wanting to create multiple machines. @EnableStateMachine defaults to bean name stateMachine and having multiple @EnableStateMachine adapters with same name would try to configure same machine. With multiple machines it'd be something like @EnableStateMachine(name = "sm1").

    2. Trouble with TaskExecutor is kinda obvious but none of a machine should not work with a code you posted because I don't see it created anywhere. Normally TaskExecutor is coming either explicitely set instance or from bean factory(if it's set) as a fallback. There's hooks for setting these in config interfaces http://docs.spring.io/spring-statemachine/docs/1.0.0.RELEASE/reference/htmlsingle/#statemachine-config-commonsettings.

    3. These samples on default use @EnableStateMachine which does context integration automatically meaning spring application context event publisher is also registered(which doesn't happen with machines from manual builder), thus normal ways to create ApplicationListener as done in https://github.com/spring-projects/spring-statemachine/blob/master/spring-statemachine-samples/src/main/java/demo/CommonConfiguration.java#L57 no longer works. You can also use StateMachineListenerAdapter and register those with machine via configuration.

    I would not try to build any apps around specific shell concepts in samples. Shell was just used to give easy way to interact with machine from a command line. I looks like you might get away from all trouble by using different bean names for all machines, i.e. @EnableStateMachine(name = "sm1").

    I'll try to create some gh issues based on these use cases. There always seem to be different ways how people try to use this stuff what we're not anticipated in our tests.