0

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";
}
melike
  • 137
  • 2
  • 11
  • There's some fixes(soon to be released 1.0.1) for some issues what you may be facing. It's difficult to comment without seeing any code so would you mind sharing some of your stuff if that's possible? – Janne Valkealahti Nov 11 '15 at 16:57
  • Hi, I added some sample code. Hope it helps. –  Nov 12 '15 at 09:05
  • 1
    I just fixed issues [gh119](https://github.com/spring-projects/spring-statemachine/issues/119) and [gh120](https://github.com/spring-projects/spring-statemachine/issues/120) related to your problems. Hopefully we get 1.0.1 out next week. – Janne Valkealahti Nov 14 '15 at 16:23

2 Answers2

1

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.

Janne Valkealahti
  • 2,552
  • 1
  • 14
  • 12
0

I tried configuring the state machine factory with 2 sets of configuration using repository model factory, each giving a name.

Then when use the persist recipe, I need to pass a string parameter of statemachine id for the statemachine factory to get the statemachine instance and pass it to construct the handler, and use the handler to update as in the example.

So the problem comes to how to configure the handler bean with an argument. Instead of @Autowiredthe handler or anything requires the handler, get it with beanFactory.getBean().

It worked for me in the recipe implementation alone. But technically it should work with the configuration using model factory.

Zii
  • 45
  • 2
  • 10