diff --git a/docs/src/reference/asciidoc/sm.adoc b/docs/src/reference/asciidoc/sm.adoc index b58b6cc2..917aa384 100644 --- a/docs/src/reference/asciidoc/sm.adoc +++ b/docs/src/reference/asciidoc/sm.adoc @@ -415,6 +415,64 @@ is attached automatically and then a default _TaskExecutor_ can be found from there. If instances are used outside of a spring application context these methods must be used to setup needed facilities. +[[sm-deferevents]] +== Using Deferred Events +When en 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. Lets +take this with a simple example. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests2.java[tags=snippetE] +---- + +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. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests2.java[tags=snippetF] +---- + +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. + [[sm-scopes]] == Using Scopes Support for scopes in a state machine is very limited but it is possible diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java index 0a47bb3e..d7a91cc2 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java @@ -152,6 +152,11 @@ public abstract class AbstractState implements State { return false; } + @Override + public boolean shouldDefer(Message event) { + return deferred != null && deferred.contains(event.getPayload()); + } + @Override public abstract void exit(StateContext context); diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/RegionState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/RegionState.java index c2e4f940..6d6dcc27 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/RegionState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/RegionState.java @@ -107,6 +107,25 @@ public class RegionState extends AbstractState { return accept; } + @Override + public boolean shouldDefer(Message event) { + boolean defer = true; + if (getRegions() != null) { + for (Region r : getRegions()) { + State state = r.getState(); + if (state != null) { + Collection deferredEvents = state.getDeferredEvents(); + if (deferredEvents != null && deferredEvents.contains(event.getPayload())) { + defer = defer & true; + } else { + defer = false; + } + } + } + } + return defer; + } + @Override public void exit(StateContext context) { for (Region region : getRegions()) { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/State.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/State.java index 728de99e..ec0babc1 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/State.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/State.java @@ -39,6 +39,14 @@ public interface State { */ boolean sendEvent(Message event); + /** + * Checks if state wants to defer an event. + * + * @param event the wrapped event + * @return true if event should be deferred + */ + boolean shouldDefer(Message event); + /** * Initiate an exit sequence for the state. * diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java index 49fa9a0d..a60a7b53 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/StateMachineState.java @@ -239,6 +239,21 @@ public class StateMachineState extends AbstractState { return super.sendEvent(event); } + @Override + public boolean shouldDefer(Message event) { + StateMachine machine = getSubmachine(); + if (machine != null) { + State state = machine.getState(); + if (state != null) { + Collection deferredEvents = state.getDeferredEvents(); + if (deferredEvents != null && deferredEvents.contains(event.getPayload())) { + return true; + } + } + } + return super.shouldDefer(event); + } + private boolean isLocal(StateContext context) { Transition transition = context.getTransition(); if (transition != null && TransitionKind.LOCAL == transition.getKind() && this == transition.getTarget()) { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java index 34c6999e..7e4b15a6 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java @@ -571,35 +571,30 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo } protected boolean acceptEvent(Message message) { - boolean accepted = (currentState != null && currentState.sendEvent(message)); - if (accepted) { + if ((currentState != null && currentState.shouldDefer(message))) { + log.info("Current state " + currentState + " deferred event " + message); + stateMachineExecutor.queueDeferredEvent(message); + return true; + } + if ((currentState != null && currentState.sendEvent(message))) { return true; } if (log.isDebugEnabled()) { - log.debug("Queue event " + message); + log.debug("Queue event " + message + " " + this); } - Message defer = null; for (Transition transition : transitions) { State source = transition.getSource(); Trigger trigger = transition.getTrigger(); if (StateMachineUtils.containsAtleastOne(source.getIds(), currentState.getIds())) { if (trigger != null && trigger.evaluate(new DefaultTriggerContext(message.getPayload()))) { - stateMachineExecutor.queueTrigger(trigger, message); + stateMachineExecutor.queueEvent(message); return true; - } else if (source.getDeferredEvents() != null && source.getDeferredEvents().contains(message.getPayload())) { - defer = message; } } } - if (defer != null) { - log.info("Deferring event " + defer); - stateMachineExecutor.queueDeferredEvent(defer); - return true; - } - return false; } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java index 1d2067a3..ba6c4d91 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java @@ -128,6 +128,7 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im @Override public void queueTrigger(Trigger trigger, Message message) { + log.debug("Queue trigger " + trigger); triggerQueue.add(new TriggerQueueItem(trigger, message)); } @@ -227,10 +228,19 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im Runnable task = new Runnable() { @Override public void run() { - processEventQueue(); - processTriggerQueue(); - while (processDeferList()) { + boolean eventProcessed = false; + while (processEventQueue()) { + eventProcessed = true; processTriggerQueue(); + while (processDeferList()) { + processTriggerQueue(); + } + } + if (!eventProcessed) { + processTriggerQueue(); + while (processDeferList()) { + processTriggerQueue(); + } } taskRef.set(null); if (requestTask.getAndSet(false)) { @@ -246,32 +256,30 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im } } - private void processEventQueue() { + private boolean processEventQueue() { if (log.isDebugEnabled()) { log.debug("Process event queue, size=" + eventQueue.size()); } - Message queuedEvent = null; + Message queuedEvent = eventQueue.poll(); State currentState = stateMachine.getState(); - while ((queuedEvent = eventQueue.poll()) != null) { - Message defer = null; - for (Transition transition : transitions) { - State source = transition.getSource(); + if (queuedEvent != null) { + if ((currentState != null && currentState.shouldDefer(queuedEvent))) { + queueDeferredEvent(queuedEvent); + return true; + } + for (Transition transition : transitions) { + State source = transition.getSource(); Trigger trigger = transition.getTrigger(); if (StateMachineUtils.containsAtleastOne(source.getIds(), currentState.getIds())) { if (trigger != null && trigger.evaluate(new DefaultTriggerContext(queuedEvent.getPayload()))) { - triggerQueue.add(new TriggerQueueItem(trigger, queuedEvent)); - } else if (source.getDeferredEvents() != null - && source.getDeferredEvents().contains(queuedEvent.getPayload())) { - defer = queuedEvent; + queueTrigger(trigger, queuedEvent); + return true; } } } - if (defer != null) { - log.info("Deferring event " + defer); - deferList.addLast(defer); - } } + return false; } private void processTriggerQueue() { @@ -290,19 +298,16 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im return; } if (log.isDebugEnabled()) { - log.debug("Process trigger queue, size=" + triggerQueue.size()); + log.debug("Process trigger queue, size=" + triggerQueue.size() + " " + this); } - TriggerQueueItem queueItem = null; - // keep last message here so that we can + TriggerQueueItem queueItem = triggerQueue.poll(); + // keep message here so that we can // pass it to triggerless transitions - while ((queueItem = triggerQueue.poll()) != null) { - - State currentState = stateMachine.getState(); - - if (currentState == null) { - continue; + State currentState = stateMachine.getState(); + if (queueItem != null && currentState != null) { + if (log.isDebugEnabled()) { + log.debug("Process trigger item " + queueItem + " " + this); } - // queued message is kept on a class level order to let // triggerless transition to receive this message if it doesn't // kick in in this poll loop. @@ -351,15 +356,18 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im } } - private boolean processDeferList() { + private synchronized boolean processDeferList() { if (log.isDebugEnabled()) { log.debug("Process defer list, size=" + deferList.size()); } - boolean triggered = false; ListIterator> iterator = deferList.listIterator(); State currentState = stateMachine.getState(); while (iterator.hasNext()) { Message event = iterator.next(); + if (currentState.shouldDefer(event)) { + // if current state still defers, just continue with others + continue; + } for (Transition transition : transitions) { State source = transition.getSource(); Trigger trigger = transition.getTrigger(); @@ -367,12 +375,13 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im if (trigger != null && trigger.evaluate(new DefaultTriggerContext(event.getPayload()))) { triggerQueue.add(new TriggerQueueItem(trigger, event)); iterator.remove(); - triggered = true; + // bail out when first deferred message is causing a trigger to fire + return true; } } } } - return triggered; + return false; } private StateContext buildStateContext(Message message, Transition transition, StateMachine stateMachine) { diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/EventDeferTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/EventDeferTests.java new file mode 100644 index 00000000..83124928 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/EventDeferTests.java @@ -0,0 +1,567 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.statemachine.config.EnableStateMachine; +import org.springframework.statemachine.config.StateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; +import org.springframework.statemachine.listener.StateMachineListenerAdapter; +import org.springframework.statemachine.state.State; + +public class EventDeferTests extends AbstractStateMachineTests { + + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + + @Test + public void testDeferWithFlat() throws Exception { + context.register(Config2.class); + context.refresh(); + @SuppressWarnings("unchecked") + StateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + machine.sendEvent("E3"); + machine.sendEvent("E1"); + Object executor = TestUtils.readField("stateMachineExecutor", machine); + List readField = TestUtils.readField("deferList", executor); + assertThat(readField.size(), is(1)); + machine.sendEvent("E2"); + assertThat(readField.size(), is(2)); + } + + @Test + public void testDeferWithFlatThreadExecutor() throws Exception { + context.register(Config2.class, ExecutorConfig.class); + context.refresh(); + @SuppressWarnings("unchecked") + StateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + assertThat(listener.stateMachineStartedLatch.await(3, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedLatch.await(3, TimeUnit.SECONDS), is(true)); + + listener.reset(1, 0, 0, 0); + machine.sendEvent("E3"); + assertThat(listener.stateChangedLatch.await(3, TimeUnit.SECONDS), is(true)); + + machine.sendEvent("E1"); + machine.sendEvent("E1"); + Object executor = TestUtils.readField("stateMachineExecutor", machine); + List readField = TestUtils.readField("deferList", executor); + assertThat(readField.size(), is(2)); + machine.sendEvent("E2"); + assertThat(readField.size(), is(3)); + } + + @Test + public void testDeferWithSubsSyncExecutor() throws Exception { + context.register(Config1.class); + context.refresh(); + @SuppressWarnings("unchecked") + StateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + + assertThat(listener.stateMachineStartedLatch.await(3, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedLatch.await(3, TimeUnit.SECONDS), is(true)); + + listener.reset(0, 0, 0, 1); + machine.sendEvent("E3"); + assertThat(listener.sub3readyStateEnteredLatch.await(3, TimeUnit.SECONDS), is(true)); + assertThat(listener.sub3readyStateEnteredCount, is(1)); + + machine.sendEvent("E1"); + + Object executor = TestUtils.readField("stateMachineExecutor", machine); + List readField = TestUtils.readField("deferList", executor); + assertThat(readField.size(), is(1)); + + listener.reset(0, 0, 2, 0); + machine.sendEvent("E4"); + assertThat(listener.readyStateEnteredLatch.await(3, TimeUnit.SECONDS), is(true)); + assertThat(listener.readyStateEnteredCount, is(2)); + + assertThat(machine.getState().getIds(), contains("READY")); + } + + @Test + public void testDeferWithSubsThreadExecutor() throws Exception { + context.register(Config1.class, ExecutorConfig.class); + context.refresh(); + @SuppressWarnings("unchecked") + StateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + + assertThat(listener.stateMachineStartedLatch.await(3, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedLatch.await(3, TimeUnit.SECONDS), is(true)); + + listener.reset(0, 0, 0, 1); + machine.sendEvent("E3"); + assertThat(listener.sub3readyStateEnteredLatch.await(3, TimeUnit.SECONDS), is(true)); + assertThat(listener.sub3readyStateEnteredCount, is(1)); + + listener.reset(0, 0, 2, 0); + machine.sendEvent("E1"); + machine.sendEvent("E1"); + + Object executor = TestUtils.readField("stateMachineExecutor", machine); + List readField = TestUtils.readField("deferList", executor); + assertThat(readField.size(), is(2)); + + listener.reset(0, 0, 3, 0); + machine.sendEvent("E4"); + assertThat(listener.readyStateEnteredLatch.await(3, TimeUnit.SECONDS), is(true)); + assertThat(listener.readyStateEnteredCount, is(3)); + + assertThat(machine.getState().getIds(), contains("READY")); + } + + @Test + public void testSubNotDeferOverrideSuperTransition() throws Exception { + context.register(Config3.class); + context.refresh(); + @SuppressWarnings("unchecked") + StateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + assertThat(listener.stateMachineStartedLatch.await(3, TimeUnit.SECONDS), is(true)); + + machine.sendEvent("E1"); + assertThat(machine.getState().getIds(), contains("SUB1", "SUB11")); + + // sub doesn't defer + machine.sendEvent("E15"); + Object executor = TestUtils.readField("stateMachineExecutor", machine); + List readField = TestUtils.readField("deferList", executor); + assertThat(readField.size(), is(0)); + + assertThat(machine.getState().getIds(), contains("SUB5")); + } + + @Test + public void testSubDeferOverrideSuperTransition() throws Exception { + context.register(Config3.class); + context.refresh(); + @SuppressWarnings("unchecked") + StateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + assertThat(listener.stateMachineStartedLatch.await(3, TimeUnit.SECONDS), is(true)); + + machine.sendEvent("E1"); + assertThat(machine.getState().getIds(), contains("SUB1", "SUB11")); + + machine.sendEvent("E1112"); + assertThat(machine.getState().getIds(), contains("SUB1", "SUB12")); + + // sub defers + machine.sendEvent("E15"); + Object executor = TestUtils.readField("stateMachineExecutor", machine); + List readField = TestUtils.readField("deferList", executor); + assertThat(readField.size(), is(1)); + + assertThat(machine.getState().getIds(), contains("SUB1", "SUB12")); + + // from SUB12 to SUB11 should then cause E15 to fire in root + // causing SUB1 to SUB5 + machine.sendEvent("E1211"); + assertThat(machine.getState().getIds(), contains("SUB5")); + } + + @Test + public void testRegionOneDeferTransition() throws Exception { + context.register(Config4.class); + context.refresh(); + @SuppressWarnings("unchecked") + StateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + assertThat(listener.stateMachineStartedLatch.await(3, TimeUnit.SECONDS), is(true)); + + machine.sendEvent("E1"); + assertThat(machine.getState().getIds(), containsInAnyOrder("SUB111", "SUB1", "SUB121")); + + machine.sendEvent("E5"); + assertThat(machine.getState().getIds(), containsInAnyOrder("SUB112", "SUB1", "SUB121")); + + // regions defers + machine.sendEvent("E3"); + Object executor = TestUtils.readField("stateMachineExecutor", machine); + List readField = TestUtils.readField("deferList", executor); + assertThat(readField.size(), is(0)); + } + + @Test + public void testRegionAllDeferTransition() throws Exception { + context.register(Config4.class); + context.refresh(); + @SuppressWarnings("unchecked") + StateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + assertThat(listener.stateMachineStartedLatch.await(3, TimeUnit.SECONDS), is(true)); + + machine.sendEvent("E1"); + assertThat(machine.getState().getIds(), containsInAnyOrder("SUB111", "SUB1", "SUB121")); + + machine.sendEvent("E5"); + assertThat(machine.getState().getIds(), containsInAnyOrder("SUB112", "SUB1", "SUB121")); + + machine.sendEvent("E8"); + assertThat(machine.getState().getIds(), containsInAnyOrder("SUB112", "SUB1", "SUB122")); + + // regions defers + machine.sendEvent("E3"); + Object executor = TestUtils.readField("stateMachineExecutor", machine); + List readField = TestUtils.readField("deferList", executor); + assertThat(readField.size(), is(1)); + } + + @Test + public void testRegionNotDeferTransition() throws Exception { + context.register(Config4.class); + context.refresh(); + @SuppressWarnings("unchecked") + StateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + assertThat(listener.stateMachineStartedLatch.await(3, TimeUnit.SECONDS), is(true)); + + machine.sendEvent("E1"); + assertThat(machine.getState().getIds(), containsInAnyOrder("SUB111", "SUB1", "SUB121")); + + // regions doesn't defer + machine.sendEvent("E3"); + Object executor = TestUtils.readField("stateMachineExecutor", machine); + List readField = TestUtils.readField("deferList", executor); + assertThat(readField.size(), is(0)); + + assertThat(machine.getState().getIds(), contains("SUB2")); + } + + @Configuration + @EnableStateMachine + static class Config1 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("READY") + .state("SUB1") + .state("SUB2", "E1", "E2") + .state("SUB3", "E1", "E2") + .and() + .withStates() + .parent("SUB1") + .initial("SUB1READY") + .and() + .withStates() + .parent("SUB2") + .initial("SUB2READY") + .and() + .withStates() + .parent("SUB3") + .initial("SUB3READY"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("READY").target("SUB1") + .event("E1") + .and() + .withExternal() + .source("READY").target("SUB2") + .event("E2") + .and() + .withExternal() + .source("READY").target("SUB3") + .event("E3") + .and() + .withExternal() + .source("SUB1READY").target("READY") + .and() + .withExternal() + .source("SUB2READY").target("READY") + .and() + .withExternal() + .source("SUB3").target("READY") + .event("NOTUSED") + .and() + .withExternal() + .source("SUB3READY").target("READY") + .event("E4"); + } + + } + + @Configuration + @EnableStateMachine + static class Config2 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("READY") + .state("S1") + .state("S2") + .state("S3", "E1", "E2"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("READY").target("S1") + .event("E1") + .and() + .withExternal() + .source("READY").target("S2") + .event("E2") + .and() + .withExternal() + .source("READY").target("S3") + .event("E3") + .and() + .withExternal() + .source("S3").target("S1") + .event("E4") + .and() + .withExternal() + .source("S3").target("S2") + .event("E5") + .and() + .withExternal() + .source("S3").target("READY") + .event("E6"); + } + + } + + @Configuration + @EnableStateMachine + static class Config3 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("READY") + .state("SUB1") + .state("SUB2") + .state("SUB3") + .state("SUB4") + .state("SUB5") + .and() + .withStates() + .parent("SUB1") + .initial("SUB11") + .state("SUB12", "E15") + .and() + .withStates() + .parent("SUB2") + .initial("SUB21") + .state("SUB22") + .and() + .withStates() + .parent("SUB3") + .initial("SUB31") + .state("SUB32"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("READY").target("SUB1") + .event("E1") + .and() + .withExternal() + .source("READY").target("SUB2") + .event("E2") + .and() + .withExternal() + .source("READY").target("SUB3") + .event("E3") + .and() + .withExternal() + .source("READY").target("SUB4") + .event("E4") + .and() + .withExternal() + .source("READY").target("SUB5") + .event("E5") + .and() + .withExternal() + .source("SUB1").target("SUB5") + .event("E15") + .and() + .withExternal() + .source("SUB5").target("SUB1") + .event("E51") + .and() + .withExternal() + .source("SUB11").target("SUB12") + .event("E1112") + .and() + .withExternal() + .source("SUB12").target("SUB11") + .event("E1211"); + } + + } + + @Configuration + @EnableStateMachine + static class Config4 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("READY") + .state("SUB1") + .state("SUB2") + .and() + .withStates() + .parent("SUB1") + .initial("SUB111") + .state("SUB112", "E3", "E6") + .and() + .withStates() + .parent("SUB1") + .initial("SUB121") + .state("SUB122", "E3", "E7"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("READY").target("SUB1") + .event("E1") + .and() + .withExternal() + .source("SUB1").target("SUB2") + .event("E2") + .and() + .withExternal() + .source("SUB1").target("SUB2") + .event("E3") + .and() + .withExternal() + .source("SUB1").target("SUB2") + .event("E4") + .and() + .withExternal() + .source("SUB111").target("SUB112") + .event("E5") + .and() + .withExternal() + .source("SUB121").target("SUB122") + .event("E8"); + } + + } + + @Configuration + static class ExecutorConfig { + + @Bean(name=StateMachineSystemConstants.TASK_EXECUTOR_BEAN_NAME) + public TaskExecutor taskExecutor() { + ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); + taskExecutor.setCorePoolSize(1); + taskExecutor.setMaxPoolSize(1); + return taskExecutor; + } + + } + + static class TestListener extends StateMachineListenerAdapter { + + volatile CountDownLatch stateChangedLatch = new CountDownLatch(1); + volatile CountDownLatch stateMachineStartedLatch = new CountDownLatch(1); + volatile CountDownLatch readyStateEnteredLatch = new CountDownLatch(1); + volatile CountDownLatch sub3readyStateEnteredLatch = new CountDownLatch(1); + volatile int readyStateEnteredCount = 0; + volatile int sub3readyStateEnteredCount = 0; + + @Override + public void stateChanged(State from, State to) { + stateChangedLatch.countDown(); + } + + @Override + public void stateEntered(State state) { + if (state.getId().equals("READY")) { + readyStateEnteredCount++; + readyStateEnteredLatch.countDown(); + } else if (state.getId().equals("SUB3READY")) { + sub3readyStateEnteredCount++; + sub3readyStateEnteredLatch.countDown(); + } + } + + @Override + public void stateMachineStarted(StateMachine stateMachine) { + stateMachineStartedLatch.countDown(); + } + + public void reset(int c1, int c2, int c3, int c4) { + stateChangedLatch = new CountDownLatch(c1); + stateMachineStartedLatch = new CountDownLatch(c2); + readyStateEnteredLatch = new CountDownLatch(c3); + sub3readyStateEnteredLatch = new CountDownLatch(c4); + readyStateEnteredCount = 0; + sub3readyStateEnteredCount = 0; + } + + } + + +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests2.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests2.java index c21ec8bc..37dfbdf4 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests2.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests2.java @@ -15,18 +15,29 @@ */ package org.springframework.statemachine.docs; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.core.task.SyncTaskExecutor; +import org.springframework.core.task.TaskExecutor; import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.statemachine.AbstractStateMachineTests; import org.springframework.statemachine.StateContext; import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.StateMachineSystemConstants; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.config.EnableStateMachine; import org.springframework.statemachine.config.StateMachineBuilder; @@ -35,6 +46,8 @@ import org.springframework.statemachine.config.StateMachineConfigurerAdapter; import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; +import org.springframework.statemachine.listener.StateMachineListenerAdapter; +import org.springframework.statemachine.state.State; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -43,6 +56,11 @@ import org.springframework.web.bind.annotation.ResponseBody; public class DocsConfigurationSampleTests2 extends AbstractStateMachineTests { + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + // tag::snippetA[] @Configuration @EnableStateMachine @@ -169,4 +187,203 @@ public class DocsConfigurationSampleTests2 extends AbstractStateMachineTests { } // end::snippetD[] + @Test + public void testConfig51() throws Exception { + context.register(Config5.class, ExecutorConfig.class); + context.refresh(); + @SuppressWarnings("unchecked") + StateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + assertThat(listener.stateMachineStartedLatch.await(3, TimeUnit.SECONDS), is(true)); + assertThat(listener.readyStateEnteredLatch.await(3, TimeUnit.SECONDS), is(true)); + assertThat(listener.readyStateEnteredCount, is(1)); + listener.reset(0, 0, 2); + machine.sendEvent("DEPLOY"); + machine.sendEvent("DEPLOY"); + assertThat(listener.readyStateEnteredLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.readyStateEnteredCount, is(2)); + } + + @Test + public void testConfig52() throws Exception { + context.register(Config5.class); + context.refresh(); + @SuppressWarnings("unchecked") + StateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + assertThat(listener.stateMachineStartedLatch.await(3, TimeUnit.SECONDS), is(true)); + assertThat(listener.readyStateEnteredLatch.await(3, TimeUnit.SECONDS), is(true)); + assertThat(listener.readyStateEnteredCount, is(1)); + listener.reset(0, 0, 2); + machine.sendEvent("DEPLOY"); + machine.sendEvent("DEPLOY"); + assertThat(listener.readyStateEnteredLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.readyStateEnteredCount, is(2)); + } + + @Test + public void testConfig61() throws Exception { + context.register(Config6.class, ExecutorConfig.class); + context.refresh(); + @SuppressWarnings("unchecked") + StateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + assertThat(listener.stateMachineStartedLatch.await(3, TimeUnit.SECONDS), is(true)); + assertThat(listener.readyStateEnteredLatch.await(3, TimeUnit.SECONDS), is(true)); + assertThat(listener.readyStateEnteredCount, is(1)); + listener.reset(0, 0, 2); + machine.sendEvent("DEPLOY"); + machine.sendEvent("DEPLOY"); + assertThat(listener.readyStateEnteredLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.readyStateEnteredCount, is(2)); + } + + @Test + public void testConfig62() throws Exception { + context.register(Config6.class); + context.refresh(); + @SuppressWarnings("unchecked") + StateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestListener listener = new TestListener(); + machine.addStateListener(listener); + machine.start(); + assertThat(listener.stateMachineStartedLatch.await(3, TimeUnit.SECONDS), is(true)); + assertThat(listener.readyStateEnteredLatch.await(3, TimeUnit.SECONDS), is(true)); + assertThat(listener.readyStateEnteredCount, is(1)); + listener.reset(0, 0, 2); + machine.sendEvent("DEPLOY"); + machine.sendEvent("DEPLOY"); + assertThat(listener.readyStateEnteredLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.readyStateEnteredCount, is(2)); + } + + @Configuration + static class ExecutorConfig { + + @Bean(name=StateMachineSystemConstants.TASK_EXECUTOR_BEAN_NAME) + public TaskExecutor taskExecutor() { + ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); + taskExecutor.setCorePoolSize(1); + return taskExecutor; + } + } + +// tag::snippetE[] + @Configuration + @EnableStateMachine + static class Config5 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) + throws Exception { + states + .withStates() + .initial("READY") + .state("DEPLOYPREPARE", "DEPLOY") + .state("DEPLOYEXECUTE", "DEPLOY"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) + throws Exception { + transitions + .withExternal() + .source("READY").target("DEPLOYPREPARE") + .event("DEPLOY") + .and() + .withExternal() + .source("DEPLOYPREPARE").target("DEPLOYEXECUTE") + .and() + .withExternal() + .source("DEPLOYEXECUTE").target("READY"); + } + } +// end::snippetE[] + +// tag::snippetF[] + @Configuration + @EnableStateMachine + static class Config6 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer 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 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"); + } + } +// end::snippetF[] + + static class TestListener extends StateMachineListenerAdapter { + + volatile CountDownLatch stateChangedLatch = new CountDownLatch(1); + volatile CountDownLatch stateMachineStartedLatch = new CountDownLatch(1); + volatile CountDownLatch readyStateEnteredLatch = new CountDownLatch(1); + volatile int readyStateEnteredCount = 0; + + @Override + public void stateChanged(State from, State to) { + stateChangedLatch.countDown(); + } + + @Override + public void stateEntered(State state) { + if (state.getId().equals("READY")) { + readyStateEnteredCount++; + readyStateEnteredLatch.countDown(); + } + } + + @Override + public void stateMachineStarted(StateMachine stateMachine) { + stateMachineStartedLatch.countDown(); + } + + public void reset(int c1, int c2, int c3) { + stateChangedLatch = new CountDownLatch(c1); + stateMachineStartedLatch = new CountDownLatch(c2); + readyStateEnteredLatch = new CountDownLatch(c3); + readyStateEnteredCount = 0; + } + + } + }