Event defer for substates and regions

- Modify state structure so that nested states and orthogonal
  regions can work with deferred events.
- Change executor to work better with deferred events and try
  to provide better model for run-to-completion when execution
  happens in a threads.
- Add new event defer section in ref docs.
- Fixes #131
This commit is contained in:
Janne Valkealahti
2015-12-06 14:47:10 +00:00
parent 2569900dfb
commit b7cc1df59b
9 changed files with 937 additions and 44 deletions

View File

@@ -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 states 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

View File

@@ -152,6 +152,11 @@ public abstract class AbstractState<S, E> implements State<S, E> {
return false;
}
@Override
public boolean shouldDefer(Message<E> event) {
return deferred != null && deferred.contains(event.getPayload());
}
@Override
public abstract void exit(StateContext<S, E> context);

View File

@@ -107,6 +107,25 @@ public class RegionState<S, E> extends AbstractState<S, E> {
return accept;
}
@Override
public boolean shouldDefer(Message<E> event) {
boolean defer = true;
if (getRegions() != null) {
for (Region<S, E> r : getRegions()) {
State<S, E> state = r.getState();
if (state != null) {
Collection<E> deferredEvents = state.getDeferredEvents();
if (deferredEvents != null && deferredEvents.contains(event.getPayload())) {
defer = defer & true;
} else {
defer = false;
}
}
}
}
return defer;
}
@Override
public void exit(StateContext<S, E> context) {
for (Region<S, E> region : getRegions()) {

View File

@@ -39,6 +39,14 @@ public interface State<S, E> {
*/
boolean sendEvent(Message<E> event);
/**
* Checks if state wants to defer an event.
*
* @param event the wrapped event
* @return true if event should be deferred
*/
boolean shouldDefer(Message<E> event);
/**
* Initiate an exit sequence for the state.
*

View File

@@ -239,6 +239,21 @@ public class StateMachineState<S, E> extends AbstractState<S, E> {
return super.sendEvent(event);
}
@Override
public boolean shouldDefer(Message<E> event) {
StateMachine<S, E> machine = getSubmachine();
if (machine != null) {
State<S, E> state = machine.getState();
if (state != null) {
Collection<E> deferredEvents = state.getDeferredEvents();
if (deferredEvents != null && deferredEvents.contains(event.getPayload())) {
return true;
}
}
}
return super.shouldDefer(event);
}
private boolean isLocal(StateContext<S, E> context) {
Transition<S, E> transition = context.getTransition();
if (transition != null && TransitionKind.LOCAL == transition.getKind() && this == transition.getTarget()) {

View File

@@ -571,35 +571,30 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
}
protected boolean acceptEvent(Message<E> 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<E> defer = null;
for (Transition<S,E> transition : transitions) {
State<S,E> source = transition.getSource();
Trigger<S, E> trigger = transition.getTrigger();
if (StateMachineUtils.containsAtleastOne(source.getIds(), currentState.getIds())) {
if (trigger != null && trigger.evaluate(new DefaultTriggerContext<S, E>(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;
}

View File

@@ -128,6 +128,7 @@ public class DefaultStateMachineExecutor<S, E> extends LifecycleObjectSupport im
@Override
public void queueTrigger(Trigger<S, E> trigger, Message<E> message) {
log.debug("Queue trigger " + trigger);
triggerQueue.add(new TriggerQueueItem(trigger, message));
}
@@ -227,10 +228,19 @@ public class DefaultStateMachineExecutor<S, E> 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<S, E> extends LifecycleObjectSupport im
}
}
private void processEventQueue() {
private boolean processEventQueue() {
if (log.isDebugEnabled()) {
log.debug("Process event queue, size=" + eventQueue.size());
}
Message<E> queuedEvent = null;
Message<E> queuedEvent = eventQueue.poll();
State<S,E> currentState = stateMachine.getState();
while ((queuedEvent = eventQueue.poll()) != null) {
Message<E> defer = null;
for (Transition<S, E> transition : transitions) {
State<S, E> source = transition.getSource();
if (queuedEvent != null) {
if ((currentState != null && currentState.shouldDefer(queuedEvent))) {
queueDeferredEvent(queuedEvent);
return true;
}
for (Transition<S,E> transition : transitions) {
State<S,E> source = transition.getSource();
Trigger<S, E> trigger = transition.getTrigger();
if (StateMachineUtils.containsAtleastOne(source.getIds(), currentState.getIds())) {
if (trigger != null && trigger.evaluate(new DefaultTriggerContext<S, E>(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<S, E> 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<S,E> currentState = stateMachine.getState();
if (currentState == null) {
continue;
State<S,E> 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<S, E> 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<Message<E>> iterator = deferList.listIterator();
State<S,E> currentState = stateMachine.getState();
while (iterator.hasNext()) {
Message<E> event = iterator.next();
if (currentState.shouldDefer(event)) {
// if current state still defers, just continue with others
continue;
}
for (Transition<S, E> transition : transitions) {
State<S, E> source = transition.getSource();
Trigger<S, E> trigger = transition.getTrigger();
@@ -367,12 +375,13 @@ public class DefaultStateMachineExecutor<S, E> extends LifecycleObjectSupport im
if (trigger != null && trigger.evaluate(new DefaultTriggerContext<S, E>(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<S, E> buildStateContext(Message<E> message, Transition<S,E> transition, StateMachine<S, E> stateMachine) {

View File

@@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> 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<String, String> 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<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial("READY")
.state("S1")
.state("S2")
.state("S3", "E1", "E2");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> 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<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> 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<String, String> 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<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> 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<String, String> 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<String, String> {
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<String, String> from, State<String, String> to) {
stateChangedLatch.countDown();
}
@Override
public void stateEntered(State<String, String> state) {
if (state.getId().equals("READY")) {
readyStateEnteredCount++;
readyStateEnteredLatch.countDown();
} else if (state.getId().equals("SUB3READY")) {
sub3readyStateEnteredCount++;
sub3readyStateEnteredLatch.countDown();
}
}
@Override
public void stateMachineStarted(StateMachine<String, String> 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;
}
}
}

View File

@@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<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");
}
}
// end::snippetE[]
// tag::snippetF[]
@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");
}
}
// end::snippetF[]
static class TestListener extends StateMachineListenerAdapter<String, String> {
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<String, String> from, State<String, String> to) {
stateChangedLatch.countDown();
}
@Override
public void stateEntered(State<String, String> state) {
if (state.getId().equals("READY")) {
readyStateEnteredCount++;
readyStateEnteredLatch.countDown();
}
}
@Override
public void stateMachineStarted(StateMachine<String, String> 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;
}
}
}