antonbasic antonbasic - 3 months ago 29
Java Question

Intermittent "IllegalArgumentException: Source must be set" from hierarchical spring state machine

I'm trying to build a hierarchical state machine with spring-statemachine. It should have two orthogonal states each representing the state of two services. The following code has a reduced number of states for simplicity but the same error still occurs.

public enum MachineState {
BUFF,BUFF_OFFLINE, BUFF_ONLINE,
CB,CB_OFFLINE,CB_ONLINE
}

public enum MachineEvent {
BUFF_OFF,BUFF_ON,
CB_OFF, CB_NORESP, BUFF_NORESP, CB_ON
}

@Configuration
@EnableStateMachine
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<MachineState, MachineEvent> {


@Override
public void configure(final StateMachineConfigurationConfigurer<MachineState, MachineEvent> config)
throws Exception {
config
.withConfiguration()
.autoStartup(true);
}

@Override
public void configure(final StateMachineStateConfigurer<MachineState, MachineEvent> states)
throws Exception {
states
.withStates()
.initial(MachineState.BUFF)
.and()
.withStates()
.parent(MachineState.BUFF)
.initial(MachineState.BUFF_OFFLINE)
.state(MachineState.BUFF_ONLINE)
.and()
.withStates()
.initial(MachineState.CB)
.and()
.withStates()
.parent(MachineState.CB)
.initial(MachineState.CB_OFFLINE)
.state(MachineState.CB_ONLINE)
.and()
;
}

@Override
public void configure(final StateMachineTransitionConfigurer<MachineState, MachineEvent> transitions)
throws Exception {
transitions
.withExternal()
.source(MachineState.BUFF_OFFLINE).target(MachineState.BUFF_ONLINE)
.event(MachineEvent.BUFF_ON)
.and()

.withExternal()
.source(MachineState.BUFF_ONLINE).target(MachineState.BUFF_OFFLINE)
.event(MachineEvent.BUFF_OFF)
.and()

.withExternal()
.source(MachineState.CB_OFFLINE).target(MachineState.CB_ONLINE)
.event(MachineEvent.CB_ON)
.and()

.withExternal()
.source(MachineState.CB_ONLINE).target(MachineState.CB_OFFLINE)
.event(MachineEvent.CB_OFF)
.and()

.withInternal()
.source(MachineState.CB)
.event(MachineEvent.CB_NORESP)
.and()

.withInternal()
.source(MachineState.BUFF)
.event(MachineEvent.BUFF_NORESP)
.and()
;
}
}


First of all, have I done anything wrong in my configuration?

The error I get is the following

Caused by: java.lang.IllegalArgumentException: Source must be set
at org.springframework.util.Assert.notNull(Assert.java:115) ~[spring-core-4.3.2.RELEASE.jar:4.3.2.RELEASE]
at org.springframework.statemachine.transition.AbstractTransition.<init>(AbstractTransition.java:63) ~[spring-statemachine-core-1.1.0.RELEASE.jar:1.1.0.RELEASE]
at org.springframework.statemachine.transition.AbstractInternalTransition.<init>(AbstractInternalTransition.java:35) ~[spring-statemachine-core-1.1.0.RELEASE.jar:1.1.0.RELEASE]
at org.springframework.statemachine.transition.DefaultInternalTransition.<init>(DefaultInternalTransition.java:35) ~[spring-statemachine-core-1.1.0.RELEASE.jar:1.1.0.RELEASE]
at org.springframework.statemachine.config.AbstractStateMachineFactory.buildMachine(AbstractStateMachineFactory.java:704) ~[spring-statemachine-core-1.1.0.RELEASE.jar:1.1.0.RELEASE]
at org.springframework.statemachine.config.AbstractStateMachineFactory.getStateMachine(AbstractStateMachineFactory.java:189) ~[spring-statemachine-core-1.1.0.RELEASE.jar:1.1.0.RELEASE]
at org.springframework.statemachine.config.AbstractStateMachineFactory.getStateMachine(AbstractStateMachineFactory.java:126) ~[spring-statemachine-core-1.1.0.RELEASE.jar:1.1.0.RELEASE]
at org.springframework.statemachine.config.configuration.StateMachineConfiguration$StateMachineDelegatingFactoryBean.afterPropertiesSet(StateMachineConfiguration.java:154) ~[spring-statemachine-core-1.1.0.RELEASE.jar:1.1.0.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1637) ~[spring-beans-4.3.2.RELEASE.jar:4.3.2.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1574) ~[spring-beans-4.3.2.RELEASE.jar:4.3.2.RELEASE]


I have debugged the application and found that in spring-statemachine-core AbstractStateMachineFactory buildMachine() the stateMap is missing one of CB and BUFF states. The weirdest part is that which one seams to be random and sometimes it actually contains the whole set and I get no exception.

I tried removing both internal transitions and debugged the code and found that even though the stateMap is incomplete (and had I had a transition from that missing state it would have failed) the state machine after instantiation looks exactly as I want it to, with all states there.

Any ideas?

Sample project https://www.dropbox.com/s/qlarppnma0dq9ai/statemachineerror.tar.gz?dl=0

Answer

So to achieve what I wanted I shouldn't have used internal transitions but external transition. It is an event that can happen on any of the substates and should return the region to the initial state.

.withExternal()
    .source(MachineState.CB).target(MachineState.CB)
    .event(MachineEvent.CB_NORESP)
.and()
.withExternal()
    .source(MachineState.BUFF).target(MachineState.BUFF)
    .event(MachineEvent.BUFF_NORESP)
.and()

Though I also found a work around if internal transition was the way to go. The limitation of the bug is that you cannot have orthogonal regions at the initial state if you want to use internal transition like I did. The solution was to introduce two new states and do an automatic transition from the first to the second and have the second be a parent state for the two orthogonal regions.

@Configuration
@EnableStateMachine
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<MachineState, MachineEvent> {


    @Override
    public void configure(final StateMachineConfigurationConfigurer<MachineState, MachineEvent> config)
            throws Exception {
        config
            .withConfiguration()
            .autoStartup(true);
    }

    @Override
    public void configure(final StateMachineStateConfigurer<MachineState, MachineEvent> states)
            throws Exception {
        states
            .withStates()
                .initial(MachineState.INITIAL)
                .state(MachineState.INITIAL, init(), null)
                .state(MachineState.PARENT)
                .and()

                // Region 1 (BUFF)
                .withStates()
                    .parent(MachineState.PARENT)
                    .initial(MachineState.BUFF)
                    .and()
                    .withStates()
                        .parent(MachineState.BUFF)
                        .initial(MachineState.BUFF_OFFLINE)
                        .state(MachineState.BUFF_ONLINE)
                .and()

                // Region 2 (CB)
                .withStates()
                    .parent(MachineState.PARENT)
                    .initial(MachineState.CB)
                    .and()
                    .withStates()
                        .parent(MachineState.CB)
                        .initial(MachineState.CB_OFFLINE)
                        .state(MachineState.CB_ONLINE)
                .and()
        ;
    }

    @Override
    public void configure(final StateMachineTransitionConfigurer<MachineState, MachineEvent> transitions)
            throws Exception {
        transitions
            .withExternal()
                .source(MachineState.BUFF_OFFLINE).target(MachineState.BUFF_ONLINE)
                .event(MachineEvent.BUFF_ON)
            .and()

            .withExternal()
                .source(MachineState.BUFF_ONLINE).target(MachineState.BUFF_OFFLINE)
                .event(MachineEvent.BUFF_OFF)
            .and()

            .withExternal()
                .source(MachineState.CB_OFFLINE).target(MachineState.CB_ONLINE)
                .event(MachineEvent.CB_ON)
            .and()

            .withExternal()
                .source(MachineState.CB_ONLINE).target(MachineState.CB_OFFLINE)
                .event(MachineEvent.CB_OFF)
            .and()

            .withInternal()
                .source(MachineState.CB)
                .event(MachineEvent.CB_NORESP)
            .and()

            .withInternal()
                .source(MachineState.BUFF)
                .event(MachineEvent.BUFF_NORESP)
            .and()

            .withExternal()
                .source(MachineState.INITIAL).target(MachineState.PARENT)
                .event(MachineEvent.INIT)
            .and()
        ;
    }

    @Bean
    public Action<MachineState, MachineEvent> init() {
        return new Action<MachineState, MachineEvent>() {
            @Override
            public void execute(StateContext<MachineState, MachineEvent> context) {
                context.getStateMachine().sendEvent(MachineEvent.INIT);
            }
        };
    }
}
Comments