Search code examples
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

  • added a new schema
  • addded some test data
  • duplicated the code for Persist, PersistCommand and adapted them to my case

No big deal so far. Now to the Configuration:

  • I removed the StateMachineConfig (and with it the @EnableStateMachine Annotation)
  • addded the StateMachineConfiguration into PersistHandlerConfig as a Bean and use the Builder
  • duplicated that config and adapted it to my use case
  • obviously crated me a class like Order for my case

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:

  • The persisting does not work any longer
  • I can start the application and both state machines
  • I can use all persist and myUseCase calls but there are not working on the persistent data.
  • If I e.g. call persist process 1, the application change the state of the underlying SM to PROCESSING, but the peristed data does not change.
  • In debugging I was able to resolve that in LifecycleObjectSupport the getTaskExecutor method returns null (while in the original example the bean factory returns an instance of SyncTaskExecutor).
  • Besides it seems that the TestEventListener does not work any longer for any of the state machines
  • When I use @EnableStateMachine at any of the configurations containing a state machine bean, an NPE occurs at afterPropertiesSet of StateMachineConfiguration.

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.