diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/EnumStateMachineFactory.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/EnumStateMachineFactory.java index 4802f774..e242f0f9 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/EnumStateMachineFactory.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/EnumStateMachineFactory.java @@ -46,9 +46,9 @@ import org.springframework.statemachine.state.RegionState; import org.springframework.statemachine.state.State; import org.springframework.statemachine.state.StateMachineState; import org.springframework.statemachine.support.DefaultExtendedState; -import org.springframework.statemachine.support.StateMachineFunction; import org.springframework.statemachine.support.LifecycleObjectSupport; import org.springframework.statemachine.support.StateMachineAccess; +import org.springframework.statemachine.support.StateMachineFunction; import org.springframework.statemachine.support.tree.Tree; import org.springframework.statemachine.support.tree.Tree.Node; import org.springframework.statemachine.support.tree.TreeTraverser; @@ -161,8 +161,9 @@ public class EnumStateMachineFactory, E extends Enum> exten // TODO: don't like that we create a last machine here Collection> states = new ArrayList>(); states.add(rstate); + Transition initialTransition = new InitialTransition(rstate); EnumStateMachine m = new EnumStateMachine(states, new ArrayList>(), rstate, - null, null, defaultExtendedState); + initialTransition, null, defaultExtendedState); if (contextEvents != null) { m.setContextEventsEnabled(contextEvents); } @@ -460,22 +461,17 @@ public class EnumStateMachineFactory, E extends Enum> exten } } - // TODO: should make a proper transition - Transition initialTransition = null; - if (initialAction != null) { - initialTransition = new InitialTransition(initialState, initialAction); - } - + Transition initialTransition = new InitialTransition(initialState, initialAction); EnumStateMachine machine = new EnumStateMachine(states, transitions, initialState, initialTransition, null, defaultExtendedState); machine.setHistoryState(historyState); if (contextEvents != null) { machine.setContextEventsEnabled(contextEvents); } - machine.afterPropertiesSet(); if (beanFactory != null) { machine.setBeanFactory(beanFactory); } + machine.afterPropertiesSet(); return machine; } 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 2304fb1e..a7de8323 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 @@ -21,23 +21,16 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; -import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ListableBeanFactory; -import org.springframework.context.Lifecycle; import org.springframework.core.OrderComparator; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.core.task.TaskExecutor; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.support.MessageBuilder; @@ -58,12 +51,12 @@ import org.springframework.statemachine.state.PseudoStateContext; import org.springframework.statemachine.state.PseudoStateKind; import org.springframework.statemachine.state.PseudoStateListener; import org.springframework.statemachine.state.State; +import org.springframework.statemachine.support.StateMachineExecutor.StateMachineExecutorTransit; +import org.springframework.statemachine.transition.InitialTransition; import org.springframework.statemachine.transition.Transition; import org.springframework.statemachine.transition.TransitionKind; import org.springframework.statemachine.trigger.DefaultTriggerContext; -import org.springframework.statemachine.trigger.TimerTrigger; import org.springframework.statemachine.trigger.Trigger; -import org.springframework.statemachine.trigger.TriggerListener; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -92,30 +85,22 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo private final ExtendedState extendedState; - private final Queue> eventQueue = new ConcurrentLinkedQueue>(); - - private final LinkedList> deferList = new LinkedList>(); - private volatile State currentState; private volatile PseudoState history; - private volatile Runnable task; - - private final AtomicBoolean requestTask = new AtomicBoolean(false); - private final Map> handlers = new HashMap>(); private volatile boolean handlersInitialized; - private final Queue triggerQueue = new ConcurrentLinkedQueue(); - private final Map, Transition> triggerToTransitionMap = new HashMap, Transition>(); private final List> triggerlessTransitions = new ArrayList>(); private StateMachine relay; + private StateMachineExecutor stateMachineExecutor; + /** * Instantiates a new abstract state machine. * @@ -156,9 +141,13 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo this.states = states; this.transitions = transitions; this.initialState = initialState; - this.initialTransition = initialTransition; this.initialEvent = initialEvent; this.extendedState = extendedState != null ? extendedState : new DefaultExtendedState(); + if (initialTransition == null) { + this.initialTransition = new InitialTransition(initialState); + } else { + this.initialTransition = initialTransition; + } } @Override @@ -186,7 +175,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo return false; } boolean accepted = acceptEvent(event); - scheduleEventQueueProcessing(); + stateMachineExecutor.execute(); return accepted; } @@ -202,6 +191,8 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo Assert.state(initialState.getPseudoState() != null && initialState.getPseudoState().getKind() == PseudoStateKind.INITIAL, "Initial state's pseudostate kind must be INITIAL"); + + // process given transitions for (Transition transition : transitions) { Trigger trigger = transition.getTrigger(); if (trigger != null) { @@ -211,6 +202,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo triggerlessTransitions.add(transition); } } + for (State state : states) { if (state.isSubmachineState()) { StateMachine submachine = ((AbstractState)state).getSubmachine(); @@ -227,6 +219,32 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo history = state.getPseudoState(); } } + + DefaultStateMachineExecutor executor = new DefaultStateMachineExecutor(this, getRelayStateMachine(), extendedState, + transitions, triggerToTransitionMap, triggerlessTransitions, initialTransition, initialEvent); + if (getBeanFactory() != null) { + executor.setBeanFactory(getBeanFactory()); + } else if (getTaskExecutor() != null){ + executor.setTaskExecutor(getTaskExecutor()); + } + executor.afterPropertiesSet(); + executor.setStateMachineExecutorTransit(new StateMachineExecutorTransit() { + + @Override + public void transit(Transition t, StateContext stateContext, Message queuedMessage) { + notifyTransitionStart(t); + callHandlers(t.getSource(), t.getTarget(), queuedMessage); + if (t.getKind() == TransitionKind.INITIAL) { + switchToState(t.getTarget(), queuedMessage, null, getRelayStateMachine()); + notifyStateMachineStarted(getRelayStateMachine()); + } else if (t.getKind() != TransitionKind.INTERNAL) { + switchToState(t.getTarget(), queuedMessage, t, getRelayStateMachine()); + } + notifyTransition(t); + notifyTransitionEnd(t); + } + }); + stateMachineExecutor = executor; } @Override @@ -235,21 +253,15 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo if (currentState != null) { return; } - registerTriggerListener(); registerPseudoStateListener(); - switchToState(initialState, initialEvent, null, getRelayStateMachine()); - // TODO: it is a bit off to call handlers after switchToState for initial state - callHandlers(null, initialState, initialEvent); - // TODO: for now execute outside of switchToState - if (initialTransition != null) { - StateContext stateContext = buildStateContext(initialEvent, initialTransition, getRelayStateMachine()); - initialTransition.transit(stateContext); - } - notifyStateMachineStarted(this); + + // start fires first execution which should execute initial transition + stateMachineExecutor.start(); } @Override protected void doStop() { + stateMachineExecutor.stop(); notifyStateMachineStopped(this); currentState = null; } @@ -313,6 +325,12 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo this.relay = stateMachine; } + @Override + protected void stateChangedInRelay() { + // TODO: temp tweak, see super + stateMachineExecutor.execute(); + } + private StateMachine getRelayStateMachine() { return relay != null ? relay : this; } @@ -352,7 +370,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo if (StateMachineUtils.containsAtleastOne(source.getIds(), currentState.getIds())) { if (trigger != null && trigger.evaluate(new DefaultTriggerContext(message.getPayload()))) { - triggerQueue.add(new TriggerQueueItem(trigger, message)); + stateMachineExecutor.queueTrigger(trigger, message); return true; } else if (source.getDeferredEvents() != null && source.getDeferredEvents().contains(message.getPayload())) { defer = message; @@ -361,7 +379,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo } if (defer != null) { log.info("Deferring event " + defer); - deferList.addLast(defer); + stateMachineExecutor.queueDeferredEvent(defer); return true; } @@ -386,7 +404,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo setCurrentState(state, message, transition, true, stateMachine); } - scheduleEventQueueProcessing(); + stateMachineExecutor.execute(); if (isComplete()) { stop(); } @@ -436,6 +454,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo } void setCurrentState(State state, Message message, Transition transition, boolean exit, StateMachine stateMachine) { + State findDeep = findDeepParent(state); boolean isTargetSubOf = false; if (transition != null) { @@ -597,157 +616,6 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo state.entry(stateContext); } - private void processEventQueue() { - log.debug("Process event queue"); - Message queuedEvent = null; - while ((queuedEvent = eventQueue.poll()) != null) { - 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(queuedEvent.getPayload()))) { - triggerQueue.add(new TriggerQueueItem(trigger, queuedEvent)); - } else if (source.getDeferredEvents() != null && source.getDeferredEvents().contains(queuedEvent.getPayload())) { - defer = queuedEvent; - } - } - } - if (defer != null) { - log.info("Deferring event " + defer); - deferList.addLast(defer); - } - } - } - - private boolean processDeferList() { - log.debug("Process defer list"); - boolean triggered = false; - ListIterator> iterator = deferList.listIterator(); - while (iterator.hasNext()) { - Message event = iterator.next(); - for (Transition transition : transitions) { - State source = transition.getSource(); - Trigger trigger = transition.getTrigger(); - if (source.equals(currentState)) { - if (trigger != null && trigger.evaluate(new DefaultTriggerContext(event.getPayload()))) { - triggerQueue.add(new TriggerQueueItem(trigger, event)); - iterator.remove(); - triggered = true; - } - } - } - } - return triggered; - } - - private void scheduleEventQueueProcessing() { - TaskExecutor executor = getTaskExecutor(); - if (executor == null) { - return; - } - if (task == null) { - task = new Runnable() { - @Override - public void run() { - processEventQueue(); - processTriggerQueue(); - while (processDeferList()) { - processTriggerQueue(); - } - task = null; - if (requestTask.getAndSet(false)) { - scheduleEventQueueProcessing(); - } - } - }; - executor.execute(task); - } else { - requestTask.set(true); - } - } - - private void processTriggerQueue() { - log.debug("Process trigger queue"); - TriggerQueueItem queueItem = null; - while ((queueItem = triggerQueue.poll()) != null) { - - if (currentState == null) { - continue; - } - - Message queuedMessage = queueItem.message; - E event = queuedMessage != null ? queuedMessage.getPayload() : null; - - // need all transitions trigger could match, event trigger may match multiple - // need to go up from substates and ask if trigger transit, if not check super - ArrayList> trans = new ArrayList>(); - - if (event != null) { - ArrayList ids = new ArrayList(currentState.getIds()); - Collections.reverse(ids); - for (S id : ids) { - for (Entry, Transition> e : triggerToTransitionMap.entrySet()) { - Trigger tri = e.getKey(); - E ee = tri.getEvent(); - Transition tra = e.getValue(); - if (event == ee) { - if (tra.getSource().getId() == id && !trans.contains(tra)) { - trans.add(tra); - continue; - } - } - } - } - } - - // most likely timer - if (trans.isEmpty()) { - trans.add(triggerToTransitionMap.get(queueItem.trigger)); - } - - // go through candidates and transit max one - handleTriggerTrans(trans, queuedMessage); - } - if (currentState != null) { - // handle triggerless transitions - handleTriggerTrans(triggerlessTransitions, null); - } - } - - private void handleTriggerTrans(List> trans, Message queuedMessage) { - for (Transition t : trans) { - StateContext stateContext = buildStateContext(queuedMessage, t, getRelayStateMachine()); - if (t == null) { - continue; - } - State source = t.getSource(); - if (source == null) { - continue; - } - if (!StateMachineUtils.containsAtleastOne(source.getIds(), currentState.getIds())) { - continue; - } - boolean transit = t.transit(stateContext); - if (transit) { - // TODO: should change trasition api so that we can ask - // if transition will transit so that we can post - // accurate notifyTransitionStart - notifyTransitionStart(t); - callHandlers(t.getSource(), t.getTarget(), queuedMessage); - if (t.getKind() != TransitionKind.INTERNAL) { - switchToState(t.getTarget(), queuedMessage, t, getRelayStateMachine()); - } - notifyTransition(t); - notifyTransitionEnd(t); - break; - } - - } - - } - private void callHandlers(State sourceState, State targetState, Message message) { StateContext stateContext = buildStateContext(message, null, getRelayStateMachine()); getStateMachineHandlerResults(getStateMachineHandlers(sourceState, targetState), stateContext); @@ -847,31 +715,4 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo return handle; } - private void registerTriggerListener() { - for (final Trigger trigger : triggerToTransitionMap.keySet()) { - if (trigger instanceof TimerTrigger) { - ((TimerTrigger)trigger).addTriggerListener(new TriggerListener() { - @Override - public void triggered() { - log.debug("TimedTrigger triggered " + trigger); - triggerQueue.add(new TriggerQueueItem(trigger, null)); - scheduleEventQueueProcessing(); - } - }); - } - if (trigger instanceof Lifecycle) { - ((Lifecycle)trigger).start(); - } - } - } - - private class TriggerQueueItem { - Trigger trigger; - Message message; - public TriggerQueueItem(Trigger trigger, Message message) { - this.trigger = trigger; - this.message = message; - } - } - } 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 new file mode 100644 index 00000000..12f7a8b9 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java @@ -0,0 +1,347 @@ +/* + * 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.support; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.Lifecycle; +import org.springframework.core.task.TaskExecutor; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.statemachine.ExtendedState; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.state.State; +import org.springframework.statemachine.transition.Transition; +import org.springframework.statemachine.trigger.DefaultTriggerContext; +import org.springframework.statemachine.trigger.TimerTrigger; +import org.springframework.statemachine.trigger.Trigger; +import org.springframework.statemachine.trigger.TriggerListener; + +/** + * Default implementation of a {@link StateMachineExecutor}. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class DefaultStateMachineExecutor extends LifecycleObjectSupport implements StateMachineExecutor { + + private static final Log log = LogFactory.getLog(DefaultStateMachineExecutor.class); + + private final StateMachine stateMachine; + + private final StateMachine relayStateMachine; + + private final ExtendedState extendedState; + + private final Queue> eventQueue = new ConcurrentLinkedQueue>(); + + private final LinkedList> deferList = new LinkedList>(); + + private final Queue triggerQueue = new ConcurrentLinkedQueue(); + + private final Collection> transitions; + + private final AtomicBoolean requestTask = new AtomicBoolean(false); + + private final Map, Transition> triggerToTransitionMap; + + private final List> triggerlessTransitions; + + private final Transition initialTransition; + + private final Message initialEvent; + + private final AtomicBoolean initialHandled = new AtomicBoolean(false); + + private volatile Runnable task; + + private StateMachineExecutorTransit stateMachineExecutorTransit; + + /** + * Instantiates a new default state machine executor. + * + * @param stateMachine the state machine + * @param relayStateMachine the relay state machine + * @param extendedState the extended state + * @param transitions the transitions + * @param triggerToTransitionMap the trigger to transition map + * @param triggerlessTransitions the triggerless transitions + */ + public DefaultStateMachineExecutor(StateMachine stateMachine, StateMachine relayStateMachine, + ExtendedState extendedState, Collection> transitions, Map, Transition> triggerToTransitionMap, + List> triggerlessTransitions, Transition initialTransition, Message initialEvent) { + this.stateMachine = stateMachine; + this.relayStateMachine = relayStateMachine; + this.extendedState = extendedState; + this.triggerToTransitionMap = triggerToTransitionMap; + this.triggerlessTransitions = triggerlessTransitions; + this.transitions = transitions; + this.initialTransition = initialTransition; + this.initialEvent = initialEvent; + registerTriggerListener(); + } + + @Override + public void queueEvent(Message message) { + eventQueue.add(message); + } + + @Override + public void queueTrigger(Trigger trigger, Message message) { + triggerQueue.add(new TriggerQueueItem(trigger, message)); + } + + @Override + public void queueDeferredEvent(Message message) { + deferList.addLast(message); + } + + @Override + public void execute() { + scheduleEventQueueProcessing(); + } + + @Override + public void setStateMachineExecutorTransit(StateMachineExecutorTransit stateMachineExecutorTransit) { + this.stateMachineExecutorTransit = stateMachineExecutorTransit; + } + + @Override + protected void doStart() { + super.doStart(); + execute(); + } + + @Override + protected void doStop() { + super.doStop(); + initialHandled.set(false); + } + + private void handleTriggerTrans(List> trans, Message queuedMessage) { + for (Transition t : trans) { + if (t == null) { + continue; + } + State source = t.getSource(); + if (source == null) { + continue; + } + State currentState = stateMachine.getState(); + if (!StateMachineUtils.containsAtleastOne(source.getIds(), currentState.getIds())) { + continue; + } + StateContext stateContext = buildStateContext(queuedMessage, t, relayStateMachine); + boolean transit = t.transit(stateContext); + if (transit) { + stateMachineExecutorTransit.transit(t, stateContext, queuedMessage); + break; + } + } + } + + private void handleInitialTrans(Transition tran, Message queuedMessage) { + StateContext stateContext = buildStateContext(queuedMessage, tran, relayStateMachine); + tran.transit(stateContext); + stateMachineExecutorTransit.transit(tran, stateContext, queuedMessage); + } + + private void scheduleEventQueueProcessing() { + TaskExecutor executor = getTaskExecutor(); + if (executor == null) { + return; + } + if (task == null) { + task = new Runnable() { + @Override + public void run() { + processEventQueue(); + processTriggerQueue(); + while (processDeferList()) { + processTriggerQueue(); + } + task = null; + if (requestTask.getAndSet(false)) { + scheduleEventQueueProcessing(); + } + } + }; + executor.execute(task); + } else { + requestTask.set(true); + } + } + + private void processEventQueue() { + log.debug("Process event queue"); + Message queuedEvent = null; + State currentState = stateMachine.getState(); + while ((queuedEvent = eventQueue.poll()) != null) { + 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(queuedEvent.getPayload()))) { + triggerQueue.add(new TriggerQueueItem(trigger, queuedEvent)); + } else if (source.getDeferredEvents() != null + && source.getDeferredEvents().contains(queuedEvent.getPayload())) { + defer = queuedEvent; + } + } + } + if (defer != null) { + log.info("Deferring event " + defer); + deferList.addLast(defer); + } + } + } + + private void processTriggerQueue() { + if (!isRunning()) { + return; + } + if (!initialHandled.getAndSet(true)) { + ArrayList> trans = new ArrayList>(); + trans.add(initialTransition); + handleInitialTrans(initialTransition, initialEvent); + return; + } + log.debug("Process trigger queue"); + TriggerQueueItem queueItem = null; + while ((queueItem = triggerQueue.poll()) != null) { + + State currentState = stateMachine.getState(); + + if (currentState == null) { + continue; + } + + Message queuedMessage = queueItem.message; + E event = queuedMessage != null ? queuedMessage.getPayload() : null; + + // need all transitions trigger could match, event trigger may match + // multiple + // need to go up from substates and ask if trigger transit, if not + // check super + ArrayList> trans = new ArrayList>(); + + if (event != null) { + ArrayList ids = new ArrayList(currentState.getIds()); + Collections.reverse(ids); + for (S id : ids) { + for (Entry, Transition> e : triggerToTransitionMap.entrySet()) { + Trigger tri = e.getKey(); + E ee = tri.getEvent(); + Transition tra = e.getValue(); + if (event == ee) { + if (tra.getSource().getId() == id && !trans.contains(tra)) { + trans.add(tra); + continue; + } + } + } + } + } + + // most likely timer + if (trans.isEmpty()) { + trans.add(triggerToTransitionMap.get(queueItem.trigger)); + } + + // go through candidates and transit max one + handleTriggerTrans(trans, queuedMessage); + } + if (stateMachine.getState() != null) { + // handle triggerless transitions + handleTriggerTrans(triggerlessTransitions, null); + } + } + + private boolean processDeferList() { + log.debug("Process defer list"); + boolean triggered = false; + ListIterator> iterator = deferList.listIterator(); + State currentState = stateMachine.getState(); + while (iterator.hasNext()) { + Message event = iterator.next(); + for (Transition transition : transitions) { + State source = transition.getSource(); + Trigger trigger = transition.getTrigger(); + if (source.equals(currentState)) { + if (trigger != null && trigger.evaluate(new DefaultTriggerContext(event.getPayload()))) { + triggerQueue.add(new TriggerQueueItem(trigger, event)); + iterator.remove(); + triggered = true; + } + } + } + } + return triggered; + } + + private StateContext buildStateContext(Message message, Transition transition, StateMachine stateMachine) { + E event = message != null ? message.getPayload() : null; + MessageHeaders messageHeaders = message != null ? message.getHeaders() : new MessageHeaders( + new HashMap()); + return new DefaultStateContext(event, messageHeaders, extendedState, transition, stateMachine); + } + + private void registerTriggerListener() { + for (final Trigger trigger : triggerToTransitionMap.keySet()) { + if (trigger instanceof TimerTrigger) { + ((TimerTrigger) trigger).addTriggerListener(new TriggerListener() { + @Override + public void triggered() { + log.debug("TimedTrigger triggered " + trigger); + triggerQueue.add(new TriggerQueueItem(trigger, null)); + scheduleEventQueueProcessing(); + } + }); + } + if (trigger instanceof Lifecycle) { + ((Lifecycle) trigger).start(); + } + } + } + + private class TriggerQueueItem { + Trigger trigger; + Message message; + public TriggerQueueItem(Trigger trigger, Message message) { + this.trigger = trigger; + this.message = message; + } + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineExecutor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineExecutor.java new file mode 100644 index 00000000..974e495c --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineExecutor.java @@ -0,0 +1,98 @@ +/* + * 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.support; + +import org.springframework.messaging.Message; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.transition.Transition; +import org.springframework.statemachine.trigger.Trigger; + +/** + * Interface for a {@link StateMachine} event executor. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public interface StateMachineExecutor { + + /** + * Queue event. + * + * @param message the message + */ + void queueEvent(Message message); + + /** + * Queue trigger. + * + * @param trigger the trigger + * @param message the message + */ + void queueTrigger(Trigger trigger, Message message); + + /** + * Queue deferred event. + * + * @param message the message + */ + void queueDeferredEvent(Message message); + + /** + * Execute {@code StateMachineExecutor} logic. + */ + void execute(); + + /** + * Start executor. + * + * @see LifecycleObjectSupport#start() + */ + void start(); + + /** + * Stop executor. + * + * @see LifecycleObjectSupport#stop() + */ + void stop(); + + /** + * Sets the state machine executor transit. + * + * @param stateMachineExecutorTransit the state machine executor transit + */ + void setStateMachineExecutorTransit(StateMachineExecutorTransit stateMachineExecutorTransit); + + /** + * Callback interface when executor wants to handle transit. + */ + public interface StateMachineExecutorTransit { + + /** + * Called when executor wants to do a transit. + * + * @param transition the transition + * @param stateContext the state context + * @param message the message + */ + void transit(Transition transition, StateContext stateContext, Message message); + + } + +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineObjectSupport.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineObjectSupport.java index 2b4e06e3..4be50abd 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineObjectSupport.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineObjectSupport.java @@ -165,6 +165,16 @@ public abstract class StateMachineObjectSupport extends LifecycleObjectSup } } + protected void stateChangedInRelay() { + // TODO: this is a temporary tweak to know when state is + // changed in a submachine/regions order to give + // state machine a change to request executor login again + // which is needed when we use multiple thread. with multiple + // threads submachines may do their stuff after thread handling + // main machine has already finished its execution logic, thus + // re-scheduling is needed. + } + /** * This class is used to relay listener events from a submachines which works * as its own listener context. User only connects to main root machine and @@ -175,6 +185,7 @@ public abstract class StateMachineObjectSupport extends LifecycleObjectSup @Override public void stateChanged(State from, State to) { stateListener.stateChanged(from, to); + stateChangedInRelay(); } @Override diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/InitialTransition.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/InitialTransition.java index f19bb038..96a9da74 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/InitialTransition.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/InitialTransition.java @@ -23,19 +23,51 @@ import org.springframework.statemachine.action.Action; import org.springframework.statemachine.state.State; import org.springframework.statemachine.trigger.Trigger; +/** + * {@link Transition} used during a state machine start. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ public class InitialTransition implements Transition { private final State target; private final Collection> actions; + /** + * Instantiates a new initial transition. + * + * @param target the initial target state + */ + public InitialTransition(State target) { + this.target = target; + this.actions = null; + } + + /** + * Instantiates a new initial transition. + * + * @param target the initial target state + * @param action the initial action + */ public InitialTransition(State target, Action action) { this.target = target; ArrayList> list = new ArrayList>(); - list.add(action); + if (action != null) { + list.add(action); + } this.actions = list; } + /** + * Instantiates a new initial transition. + * + * @param target the initial target state + * @param action the initial actions + */ public InitialTransition(State target, Collection> actions) { this.target = target; this.actions = actions; @@ -73,7 +105,7 @@ public class InitialTransition implements Transition { @Override public TransitionKind getKind() { - return null; + return TransitionKind.INITIAL; } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/TransitionKind.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/TransitionKind.java index f557a992..f71d1a8b 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/TransitionKind.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/transition/TransitionKind.java @@ -18,7 +18,7 @@ package org.springframework.statemachine.transition; /** * Defines enumeration of a {@link Transition} kind. This is uses within a * transition to indicate whether its type is external, internal or local. - * + * * @author Janne Valkealahti * */ @@ -31,6 +31,9 @@ public enum TransitionKind { INTERNAL, /** Indicates a local transition kind. */ - LOCAL + LOCAL, + + /** Indicates an initial transition kind. */ + INITIAL } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java index 2595263a..fb726d60 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/AbstractStateMachineTests.java @@ -30,9 +30,11 @@ import org.springframework.core.task.SyncTaskExecutor; import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler; -import org.springframework.statemachine.StateContext; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.guard.Guard; +import org.springframework.statemachine.listener.StateMachineListenerAdapter; +import org.springframework.statemachine.state.State; /** * Base class for stace machine tests. @@ -104,6 +106,23 @@ public abstract class AbstractStateMachineTests { } + @Configuration + public static class BaseConfig2 { + + @Bean + public TaskExecutor taskExecutor() { + ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); + taskExecutor.setCorePoolSize(5); + return taskExecutor; + } + + @Bean + public TaskScheduler taskScheduler() { + return new ConcurrentTaskScheduler(); + } + + } + public static class TestEntryAction extends AbstractTestAction { public TestEntryAction() { @@ -141,6 +160,30 @@ public abstract class AbstractStateMachineTests { public static class TestAction extends AbstractTestAction { } + public static class TestSleepAction extends AbstractTestAction { + + long sleep; + long now; + + public TestSleepAction(long sleep) { + super(); + this.sleep = sleep; + } + + @Override + public void execute(StateContext context) { + now = System.currentTimeMillis(); + if (sleep > 0) { + try { + Thread.sleep(sleep); + } catch (InterruptedException e) { + } + } + super.execute(context); + } + + } + public static class TestGuard implements Guard { public CountDownLatch onEvaluateLatch = new CountDownLatch(1); @@ -183,4 +226,26 @@ public abstract class AbstractStateMachineTests { } } + protected static class TestStateMachineListener extends StateMachineListenerAdapter { + + volatile CountDownLatch stateChangedLatch = new CountDownLatch(0); + volatile CountDownLatch stateMachineStartedLatch = new CountDownLatch(3); + + @Override + public void stateChanged(State from, State to) { + stateChangedLatch.countDown(); + } + + @Override + public void stateMachineStarted(StateMachine stateMachine) { + stateMachineStartedLatch.countDown(); + } + + void reset(int c1, int c2) { + stateChangedLatch = new CountDownLatch(c1); + stateMachineStartedLatch = new CountDownLatch(c2); + } + + } + } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/EnumStateMachineTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/EnumStateMachineTests.java index a8b5ce99..4b8983a4 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/EnumStateMachineTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/EnumStateMachineTests.java @@ -173,7 +173,8 @@ public class EnumStateMachineTests extends AbstractStateMachineTests { @Test public void testInternalTransitions() { - State stateSI = new EnumState(TestStates.SI); + PseudoState pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); + State stateSI = new EnumState(TestStates.SI, pseudoState); Collection> states = new ArrayList>(); states.add(stateSI); @@ -190,6 +191,7 @@ public class EnumStateMachineTests extends AbstractStateMachineTests { SyncTaskExecutor taskExecutor = new SyncTaskExecutor(); EnumStateMachine machine = new EnumStateMachine(states, transitions, stateSI); machine.setTaskExecutor(taskExecutor); + machine.afterPropertiesSet(); machine.start(); machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build()); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/RegionMachineTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/RegionMachineTests.java index 90f86cbc..41b6a526 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/RegionMachineTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/RegionMachineTests.java @@ -17,14 +17,15 @@ package org.springframework.statemachine; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.lessThan; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import java.util.ArrayList; import java.util.Collection; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.junit.Test; @@ -38,7 +39,6 @@ import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; import org.springframework.statemachine.event.StateMachineEventPublisherConfiguration; -import org.springframework.statemachine.listener.StateMachineListenerAdapter; import org.springframework.statemachine.region.Region; import org.springframework.statemachine.state.DefaultPseudoState; import org.springframework.statemachine.state.EnumState; @@ -47,6 +47,7 @@ import org.springframework.statemachine.state.PseudoStateKind; import org.springframework.statemachine.state.RegionState; import org.springframework.statemachine.state.State; import org.springframework.statemachine.transition.DefaultExternalTransition; +import org.springframework.statemachine.transition.InitialTransition; import org.springframework.statemachine.transition.Transition; import org.springframework.statemachine.trigger.EventTrigger; @@ -101,7 +102,8 @@ public class RegionMachineTests extends AbstractStateMachineTests { transitions.add(transitionFromS2ToS3); SyncTaskExecutor taskExecutor = new SyncTaskExecutor(); - EnumStateMachine machine = new EnumStateMachine(states, transitions, stateSI); + Transition initialTransition = new InitialTransition(stateSI); + EnumStateMachine machine = new EnumStateMachine(states, transitions, stateSI, initialTransition, null, null); machine.setTaskExecutor(taskExecutor); machine.afterPropertiesSet(); machine.start(); @@ -130,7 +132,7 @@ public class RegionMachineTests extends AbstractStateMachineTests { public void testMultiRegionBuildRaw() throws Exception { SyncTaskExecutor taskExecutor = new SyncTaskExecutor(); PseudoState pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); - State stateSI = new EnumState(TestStates.SI); + State stateSI = new EnumState(TestStates.SI, pseudoState); TestEntryAction entryActionS111 = new TestEntryAction("S111"); TestExitAction exitActionS111 = new TestExitAction("S111"); @@ -164,7 +166,8 @@ public class RegionMachineTests extends AbstractStateMachineTests { DefaultExternalTransition transitionFromS111ToS112 = new DefaultExternalTransition(stateS111, stateS112, null, TestEvents.E2, null, new EventTrigger(TestEvents.E2)); transitions11.add(transitionFromS111ToS112); - EnumStateMachine machine11 = new EnumStateMachine(states11, transitions11, stateS111); + Transition initialTransition11 = new InitialTransition(stateS111); + EnumStateMachine machine11 = new EnumStateMachine(states11, transitions11, stateS111, initialTransition11, null, null); machine11.setTaskExecutor(taskExecutor); machine11.afterPropertiesSet(); @@ -175,7 +178,8 @@ public class RegionMachineTests extends AbstractStateMachineTests { DefaultExternalTransition transitionFromSIToS121 = new DefaultExternalTransition(stateSI, stateS111, null, TestEvents.E3, null, new EventTrigger(TestEvents.E3)); transitions12.add(transitionFromSIToS121); - EnumStateMachine machine12 = new EnumStateMachine(states12, transitions12, stateS121); + Transition initialTransition12 = new InitialTransition(stateS121); + EnumStateMachine machine12 = new EnumStateMachine(states12, transitions12, stateS121, initialTransition12, null, null); machine12.setTaskExecutor(taskExecutor); machine12.afterPropertiesSet(); @@ -190,7 +194,8 @@ public class RegionMachineTests extends AbstractStateMachineTests { DefaultExternalTransition transitionFromSIToRegionstate = new DefaultExternalTransition(stateSI, stateR, null, TestEvents.E1, null, new EventTrigger(TestEvents.E1)); transitions.add(transitionFromSIToRegionstate); - EnumStateMachine machine = new EnumStateMachine(states, transitions, stateR); + Transition initialTransition = new InitialTransition(stateR); + EnumStateMachine machine = new EnumStateMachine(states, transitions, stateR, initialTransition, null, null); machine.setTaskExecutor(taskExecutor); machine.afterPropertiesSet(); @@ -221,7 +226,7 @@ public class RegionMachineTests extends AbstractStateMachineTests { @Test public void testMultiRegion() throws Exception { - context.register(BaseConfig.class, StateMachineEventPublisherConfiguration.class, Config1.class); + context.register(StateMachineEventPublisherConfiguration.class, Config1.class); context.refresh(); assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); @SuppressWarnings("unchecked") @@ -253,7 +258,7 @@ public class RegionMachineTests extends AbstractStateMachineTests { @SuppressWarnings("unchecked") @Test public void testRegionsInNestedState() throws Exception { - context.register(BaseConfig.class, StateMachineEventPublisherConfiguration.class, Config2.class); + context.register(StateMachineEventPublisherConfiguration.class, Config2.class); context.refresh(); EnumStateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); @@ -261,11 +266,85 @@ public class RegionMachineTests extends AbstractStateMachineTests { Collection states = TestUtils.readField("states", machine); assertThat(states.size(), is(3)); assertThat(states, containsInAnyOrder(instanceOf(EnumState.class), instanceOf(EnumState.class), instanceOf(RegionState.class))); + TestStateMachineListener listener = context.getBean(TestStateMachineListener.class); + machine.addStateListener(listener); machine.start(); + listener.reset(3, 0); machine.sendEvent(TestEvents.E1); + assertThat(listener.stateChangedLatch.await(5, TimeUnit.SECONDS), is(true)); assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S2, TestStates.S20, TestStates.S30)); } + @Test + public void testParallelRegionExecution() throws Exception { + context.register(StateMachineEventPublisherConfiguration.class, Config3.class, BaseConfig2.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + @SuppressWarnings("unchecked") + EnumStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + assertThat(machine, notNullValue()); + TestSleepAction action1 = context.getBean("action1", TestSleepAction.class); + TestSleepAction action2 = context.getBean("action2", TestSleepAction.class); + TestStateMachineListener listener = context.getBean(TestStateMachineListener.class); + machine.addStateListener(listener); + machine.start(); + assertThat(listener.stateMachineStartedLatch.await(5, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S10, TestStates.S20)); + + listener.reset(2, 0); + machine.sendEvent(TestEvents.E1); + assertThat(listener.stateChangedLatch.await(5, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S11, TestStates.S21)); + + listener.reset(1, 0); + machine.sendEvent(TestEvents.E2); + assertThat(listener.stateChangedLatch.await(5, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S10, TestStates.S21)); + + listener.reset(1, 0); + machine.sendEvent(TestEvents.E3); + assertThat(listener.stateChangedLatch.await(5, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S10, TestStates.S20)); + + // check that actions are called and that both are executed + // within a time which is less than their sleep time, + // indicating that we must have paralled execution + assertThat(action1.now, greaterThan(0l)); + assertThat(action2.now, greaterThan(0l)); + assertThat(Math.abs(action1.now-action2.now), lessThan(1999l)); + } + + @Test + public void testParallelRegionExecutionInInitialState() throws Exception { + context.register(StateMachineEventPublisherConfiguration.class, Config4.class, BaseConfig2.class); + context.refresh(); + assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE)); + @SuppressWarnings("unchecked") + EnumStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); + assertThat(machine, notNullValue()); + TestSleepAction action1 = context.getBean("action1", TestSleepAction.class); + TestSleepAction action2 = context.getBean("action2", TestSleepAction.class); + TestStateMachineListener listener = context.getBean(TestStateMachineListener.class); + machine.addStateListener(listener); + machine.start(); + assertThat(listener.stateMachineStartedLatch.await(5, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S10, TestStates.S20)); + + listener.reset(2, 0); + machine.sendEvent(TestEvents.E1); + assertThat(listener.stateChangedLatch.await(5, TimeUnit.SECONDS), is(true)); + assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S11, TestStates.S21)); + + // check that actions are called and that both are executed + // within a time which is less than their sleep time, + // indicating that we must have paralled execution + assertThat(action1.now, greaterThan(0l)); + assertThat(action2.now, greaterThan(0l)); + assertThat(Math.abs(action1.now-action2.now), lessThan(1999l)); + } + @Configuration @EnableStateMachine static class Config1 extends EnumStateMachineConfigurerAdapter { @@ -350,27 +429,117 @@ public class RegionMachineTests extends AbstractStateMachineTests { .event(TestEvents.E1); } + @Bean + public TestStateMachineListener testStateMachineListener() { + return new TestStateMachineListener(); + } + } - - private static class TestStateMachineListener extends StateMachineListenerAdapter { - - volatile CountDownLatch stateChangedLatch = new CountDownLatch(0); - volatile CountDownLatch stateMachineStartedLatch = new CountDownLatch(3); + @Configuration + @EnableStateMachine + static class Config3 extends EnumStateMachineConfigurerAdapter { @Override - public void stateChanged(State from, State to) { - stateChangedLatch.countDown(); + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S10) + .state(TestStates.S10) + .state(TestStates.S11, action1(), null) + .and() + .withStates() + .initial(TestStates.S20) + .state(TestStates.S20) + .state(TestStates.S21, action2(), null); } @Override - public void stateMachineStarted(StateMachine stateMachine) { - stateMachineStartedLatch.countDown(); + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S10) + .target(TestStates.S11) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S11) + .target(TestStates.S10) + .event(TestEvents.E2) + .and() + .withExternal() + .source(TestStates.S20) + .target(TestStates.S21) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S21) + .target(TestStates.S20) + .event(TestEvents.E3); } - void reset(int c1, int c2) { - stateChangedLatch = new CountDownLatch(c1); - stateMachineStartedLatch = new CountDownLatch(c2); + @Bean + public TestStateMachineListener testStateMachineListener() { + return new TestStateMachineListener(); + } + + @Bean + public TestSleepAction action1() { + return new TestSleepAction(2000); + } + + @Bean + public TestSleepAction action2() { + return new TestSleepAction(2000); + } + + } + + @Configuration + @EnableStateMachine + static class Config4 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S10) + .state(TestStates.S10, action1(), null) + .state(TestStates.S11) + .and() + .withStates() + .initial(TestStates.S20) + .state(TestStates.S20, action2(), null) + .state(TestStates.S21); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S10) + .target(TestStates.S11) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S20) + .target(TestStates.S21) + .event(TestEvents.E1); + } + + @Bean + public TestStateMachineListener testStateMachineListener() { + return new TestStateMachineListener(); + } + + @Bean + public TestSleepAction action1() { + return new TestSleepAction(2000); + } + + @Bean + public TestSleepAction action2() { + return new TestSleepAction(2000); } } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/RelayTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/RelayTests.java index 44dbde62..daf4aa96 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/RelayTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/RelayTests.java @@ -15,10 +15,13 @@ */ package org.springframework.statemachine; +import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; +import java.util.concurrent.TimeUnit; + import org.junit.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -44,8 +47,12 @@ public class RelayTests extends AbstractStateMachineTests { EnumStateMachine machine = context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, EnumStateMachine.class); assertThat(machine, notNullValue()); + TestStateMachineListener listener = new TestStateMachineListener(); + machine.addStateListener(listener); machine.start(); + listener.reset(3, 0); machine.sendEvent(TestEvents.E1); + assertThat(listener.stateChangedLatch.await(5, TimeUnit.SECONDS), is(true)); assertThat(machine.getState().getIds(), contains(TestStates.S2, TestStates.S21)); } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/SubStateMachineTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/SubStateMachineTests.java index 3e6ff7bc..ef06d828 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/SubStateMachineTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/SubStateMachineTests.java @@ -131,9 +131,11 @@ public class SubStateMachineTests extends AbstractStateMachineTests { SyncTaskExecutor taskExecutor = new SyncTaskExecutor(); machine.setTaskExecutor(taskExecutor); machine.afterPropertiesSet(); - machine.start(); submachine1.setTaskExecutor(taskExecutor); + submachine1.afterPropertiesSet(); submachine11.setTaskExecutor(taskExecutor); + submachine11.afterPropertiesSet(); + machine.start(); machine.sendEvent(TestEvents.E1); @@ -221,8 +223,9 @@ public class SubStateMachineTests extends AbstractStateMachineTests { SyncTaskExecutor taskExecutor = new SyncTaskExecutor(); machine.setTaskExecutor(taskExecutor); machine.afterPropertiesSet(); - machine.start(); submachine11.setTaskExecutor(taskExecutor); + submachine11.afterPropertiesSet(); + machine.start(); machine.sendEvent(TestEvents.E1); @@ -318,9 +321,11 @@ public class SubStateMachineTests extends AbstractStateMachineTests { SyncTaskExecutor taskExecutor = new SyncTaskExecutor(); machine.setTaskExecutor(taskExecutor); machine.afterPropertiesSet(); - machine.start(); submachine1.setTaskExecutor(taskExecutor); + submachine1.afterPropertiesSet(); submachine11.setTaskExecutor(taskExecutor); + submachine11.afterPropertiesSet(); + machine.start(); machine.sendEvent(TestEvents.E1); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/RegionStateTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/RegionStateTests.java index 948a5ff0..18650ade 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/RegionStateTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/RegionStateTests.java @@ -41,8 +41,8 @@ public class RegionStateTests extends AbstractStateMachineTests { @Test public void testSimpleRegionState() { - - State stateSI = new EnumState(TestStates.SI); + PseudoState pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); + State stateSI = new EnumState(TestStates.SI, pseudoState); State stateS1 = new EnumState(TestStates.S1); State stateS2 = new EnumState(TestStates.S2); State stateS3 = new EnumState(TestStates.S3); @@ -71,6 +71,7 @@ public class RegionStateTests extends AbstractStateMachineTests { SyncTaskExecutor taskExecutor = new SyncTaskExecutor(); EnumStateMachine machine = new EnumStateMachine(states, transitions, stateSI); machine.setTaskExecutor(taskExecutor); + machine.afterPropertiesSet(); machine.start(); Collection> regions = new ArrayList>(); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/SubmachineStateTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/SubmachineStateTests.java index 724c231a..2f62a108 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/SubmachineStateTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/state/SubmachineStateTests.java @@ -55,8 +55,8 @@ public class SubmachineStateTests extends AbstractStateMachineTests { @Test public void testSimpleSubmachineState() { - - State stateSI = new EnumState(TestStates.SI); + PseudoState pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); + State stateSI = new EnumState(TestStates.SI, pseudoState); State stateS1 = new EnumState(TestStates.S1); State stateS2 = new EnumState(TestStates.S2); State stateS3 = new EnumState(TestStates.S3); @@ -85,6 +85,7 @@ public class SubmachineStateTests extends AbstractStateMachineTests { SyncTaskExecutor taskExecutor = new SyncTaskExecutor(); EnumStateMachine machine = new EnumStateMachine(states, transitions, stateSI); machine.setTaskExecutor(taskExecutor); + machine.afterPropertiesSet(); machine.start(); StateMachineState state = new StateMachineState(TestStates.S4, machine); diff --git a/spring-statemachine-samples/showcase/src/test/java/demo/showcase/ShowcaseTests.java b/spring-statemachine-samples/showcase/src/test/java/demo/showcase/ShowcaseTests.java index 494b7a59..819f2682 100644 --- a/spring-statemachine-samples/showcase/src/test/java/demo/showcase/ShowcaseTests.java +++ b/spring-statemachine-samples/showcase/src/test/java/demo/showcase/ShowcaseTests.java @@ -76,7 +76,7 @@ public class ShowcaseTests { assertThat(listener.statesExited.size(), is(2)); assertThat(listener.statesExited.get(0).getId(), is(States.S11)); assertThat(listener.statesExited.get(1).getId(), is(States.S1)); - assertThat(listener.transitionCount, is(1)); + assertThat(listener.transitionCount, is(2)); } @Test diff --git a/spring-statemachine-samples/tasks/src/main/java/demo/tasks/Application.java b/spring-statemachine-samples/tasks/src/main/java/demo/tasks/Application.java index 89c6b82e..1b45b463 100644 --- a/spring-statemachine-samples/tasks/src/main/java/demo/tasks/Application.java +++ b/spring-statemachine-samples/tasks/src/main/java/demo/tasks/Application.java @@ -8,6 +8,8 @@ import java.util.Map; 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.shell.Bootstrap; import org.springframework.statemachine.StateContext; import org.springframework.statemachine.action.Action; @@ -127,8 +129,7 @@ public class Application { @Override public void execute(StateContext context) { Map variables = context.getExtendedState().getVariables(); - if (variables.get("T1").equals(false)) { - variables.put("T1", true); + if (variables.get("T1").equals(true)) { context.getStateMachine().sendEvent(Events.CONTINUE); } else { context.getStateMachine().sendEvent(Events.FALLBACK); @@ -157,6 +158,13 @@ public class Application { return new Tasks(); } + @Bean + public TaskExecutor taskExecutor() { + ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); + taskExecutor.setCorePoolSize(5); + return taskExecutor; + } + } //end::snippetA[] diff --git a/spring-statemachine-samples/tasks/src/main/java/demo/tasks/Tasks.java b/spring-statemachine-samples/tasks/src/main/java/demo/tasks/Tasks.java index 37bdc028..cd307343 100644 --- a/spring-statemachine-samples/tasks/src/main/java/demo/tasks/Tasks.java +++ b/spring-statemachine-samples/tasks/src/main/java/demo/tasks/Tasks.java @@ -54,19 +54,25 @@ public class Tasks { @StatesOnTransition(target = States.T1) public void taskT1(ExtendedState extendedState) { log.info("run task on T1"); + sleep(2000); extendedState.getVariables().put("T1", tasks.get("T1")); + log.info("run task on T1 done"); } @StatesOnTransition(target = States.T2) public void taskT2(ExtendedState extendedState) { log.info("run task on T2"); + sleep(2000); extendedState.getVariables().put("T2", tasks.get("T2")); + log.info("run task on T2 done"); } @StatesOnTransition(target = States.T3) public void taskT3(ExtendedState extendedState) { log.info("run task on T3"); + sleep(2000); extendedState.getVariables().put("T3", tasks.get("T3")); + log.info("run task on T3 done"); } @StatesOnTransition(target = States.AUTOMATIC) @@ -87,6 +93,13 @@ public class Tasks { } } + private static void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + } + } + @Override public String toString() { return "Tasks " + tasks; diff --git a/spring-statemachine-samples/tasks/src/test/java/demo/tasks/TasksTests.java b/spring-statemachine-samples/tasks/src/test/java/demo/tasks/TasksTests.java index a0cb0b4f..3b4125b1 100644 --- a/spring-statemachine-samples/tasks/src/test/java/demo/tasks/TasksTests.java +++ b/spring-statemachine-samples/tasks/src/test/java/demo/tasks/TasksTests.java @@ -46,22 +46,22 @@ public class TasksTests { @Test public void testRunOnce() throws InterruptedException { - listener.reset(3, 0, 0); + listener.reset(9, 0, 0); tasks.run(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedLatch.await(6, TimeUnit.SECONDS), is(true)); assertThat(machine.getState().getIds(), contains(States.READY)); } @Test public void testRunTwice() throws InterruptedException { - listener.reset(3, 0, 0); + listener.reset(9, 0, 0); tasks.run(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedLatch.await(6, TimeUnit.SECONDS), is(true)); assertThat(machine.getState().getIds(), contains(States.READY)); - listener.reset(3, 0, 0); + listener.reset(9, 0, 0); tasks.run(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedLatch.await(6, TimeUnit.SECONDS), is(true)); assertThat(machine.getState().getIds(), contains(States.READY)); } @@ -70,19 +70,19 @@ public class TasksTests { listener.reset(11, 0, 0); tasks.fail("T1"); tasks.run(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedLatch.await(6, TimeUnit.SECONDS), is(true)); assertThat(listener.stateChangedCount, is(11)); assertThat(machine.getState().getIds(), contains(States.READY)); } @Test public void testFailManualFix() throws InterruptedException { - listener.reset(3, 0, 0); + listener.reset(9, 0, 0); tasks.fail("T2"); tasks.run(); tasks.fix("T2"); tasks.cont(); - assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedLatch.await(6, TimeUnit.SECONDS), is(true)); assertThat(machine.getState().getIds(), contains(States.READY)); }