14. Using Deferred Events

When an event is sent it may fire an EventTrigger which then may cause a transition to happen if a state machine is in a state where trigger is evaluated successfully. Normally this may lead to a situation where an event is not accepted and is dropped. However it may be desirable to postpone this event until a state machine enters other state, in which it is possible to accept that event. In other words an event simply arrives at an inconvenient time.

Spring Statemachine provides a mechanism for deferring events for later processing. Every state can have a list of deferred events. If an event in the current state’s deferred event list occurs, the event will be saved (deferred) for future processing until a state is entered that does not list the event in its deferred event list. When such a state is entered, the state machine will automatically recall any saved events that are no longer deferred and will then either consume or discard these events. It is possible for a superstate to have a transition defined on an event that is deferred by a substate. Following same hierarchical state machines concepts, the substate takes precedence over the superstate, the event will be deferred and the transition for the superstate will not be executed. With orthogonal regions where one orthogonal region defers an event and another accepts the event, the accept takes precedence and the event is consumed and not deferred.

The most obvious use case for event deferring is when an event is causing a transition into a particular state and state machine is then returned back to its original state where second event should cause a same transition. Let’s take this with a simple example.

@Configuration
@EnableStateMachine
static class Config5 extends StateMachineConfigurerAdapter<String, String> {

    @Override
    public void configure(StateMachineStateConfigurer<String, String> states)
            throws Exception {
        states
            .withStates()
                .initial("READY")
                .state("DEPLOYPREPARE", "DEPLOY")
                .state("DEPLOYEXECUTE", "DEPLOY");
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<String, String> transitions)
            throws Exception {
        transitions
            .withExternal()
                .source("READY").target("DEPLOYPREPARE")
                .event("DEPLOY")
                .and()
            .withExternal()
                .source("DEPLOYPREPARE").target("DEPLOYEXECUTE")
                .and()
            .withExternal()
                .source("DEPLOYEXECUTE").target("READY");
    }
}

In above state machine has state READY which indicates that machine is ready to process events which would take it into a DEPLOY state where the actual deployment would happen. After deploy actions has been executed machine is then returned back into a READY state. Sending multiple events in a READY state is not causing any trouble if machine is using synchronous executor because event sending would block between event calls. However if executor is using threads then other events may get lost because machine is no longer in a state where event could be processed. Thus deferring some of these events allows machine to preserve these events.

@Configuration
@EnableStateMachine
static class Config6 extends StateMachineConfigurerAdapter<String, String> {

    @Override
    public void configure(StateMachineStateConfigurer<String, String> states)
            throws Exception {
        states
            .withStates()
                .initial("READY")
                .state("DEPLOY", "DEPLOY")
                .state("DONE")
                .and()
                .withStates()
                    .parent("DEPLOY")
                    .initial("DEPLOYPREPARE")
                    .state("DEPLOYPREPARE", "DONE")
                    .state("DEPLOYEXECUTE");
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<String, String> transitions)
            throws Exception {
        transitions
            .withExternal()
                .source("READY").target("DEPLOY")
                .event("DEPLOY")
                .and()
            .withExternal()
                .source("DEPLOYPREPARE").target("DEPLOYEXECUTE")
                .and()
            .withExternal()
                .source("DEPLOYEXECUTE").target("READY")
                .and()
            .withExternal()
                .source("READY").target("DONE")
                .event("DONE")
                .and()
            .withExternal()
                .source("DEPLOY").target("DONE")
                .event("DONE");
    }
}

In above state machine which is using nested states instead of a flat state model, event DEPLOY can be deferred directly in a substate. It is also showing concept of deferring event DONE in one of a sub-states which would then override anonymous transition between DEPLOY and DONE states if state machine happens to be in a DEPLOYPREPARE state when DONE event is dispatched. In DEPLOYEXECUTE state DONE event is not deferred, thus event would be handled in a super state.