Conflicting transitions cause undefined order behaviour

- Add new TransitionConflightPolicy and allow to configure
  it via an adapter interfaces.
- For now, use it in TransitionComparator which sorts transitions
  based on source states. This gives PARENT and CHILD sorting
  and allows to choose conflicting transitions either from parent
  or child.
- Add new sync thread used in submachines so that one thread is used
  in a whole call stach per machine(regions still get their own as well
  as a root machine).
- Fixes #456
This commit is contained in:
Janne Valkealahti
2017-12-19 13:09:41 +00:00
parent f4f904bb6e
commit 7c8b93bd45
14 changed files with 978 additions and 36 deletions

View File

@@ -159,6 +159,8 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
*/
@SuppressWarnings("unchecked")
public StateMachine<S, E> getStateMachine(UUID uuid, String machineId) {
ArrayList<StateMachine<S, E>> machines = new ArrayList<>();
StateMachineModel<S, E> stateMachineModel = resolveStateMachineModel(machineId);
if (stateMachineModel.getConfigurationData().isVerifierEnabled()) {
StateMachineModelVerifier<S, E> verifier = stateMachineModel.getConfigurationData().getVerifier();
@@ -221,6 +223,7 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
contextEvents, defaultExtendedState, stateMachineModel.getTransitionsData(), resolveTaskExecutor(stateMachineModel),
resolveTaskScheduler(stateMachineModel), machineId, null, stateMachineModel);
regionStack.push(new MachineStackItem<S, E>(machine));
machines.add(machine);
}
Collection<Region<S, E>> regions = new ArrayList<Region<S, E>>();
@@ -244,11 +247,13 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
machineId != null ? machineId : stateMachineModel.getConfigurationData().getMachineId(),
uuid, stateMachineModel);
machine = m;
machines.add(m);
}
} else {
machine = buildMachine(machineMap, stateMap, holderList, stateDatas, transitionsData, resolveBeanFactory(stateMachineModel), contextEvents,
defaultExtendedState, stateMachineModel.getTransitionsData(), resolveTaskExecutor(stateMachineModel), resolveTaskScheduler(stateMachineModel),
machineId, uuid, stateMachineModel);
machines.add(machine);
if (peek.isInitial() || (!peek.isInitial() && !machineMap.containsKey(peek.getParent()))) {
machineMap.put(peek.getParent(), machine);
}
@@ -289,6 +294,35 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
});
}
// set parent machines for each built machine
for (Entry<Object, StateMachine<S, E>> mme : machineMap.entrySet()) {
StateMachine<S, E> m = null;
if (mme.getKey() != null) {
Object sParent = null;
for (StateData<S, E> sd : stateMachineModel.getStatesData().getStateData()) {
if (ObjectUtils.nullSafeEquals(sd.getState(), mme.getKey())) {
sParent = sd.getParent();
break;
}
}
m = machineMap.get(sParent);
}
final StateMachine<S, E> mm = m;
mme.getValue().getStateMachineAccessor().doWithRegion(new StateMachineFunction<StateMachineAccess<S ,E>>(){
@Override
public void apply(StateMachineAccess<S, E> function) {
function.setParentMachine(mm);
}
});
}
// init built machines
for (StateMachine<S, E> m : machines) {
((LifecycleObjectSupport)m).afterPropertiesSet();
}
// TODO: should error out if sec is enabled but spring-security is not in cp
if (stateMachineModel.getConfigurationData().isSecurityEnabled()) {
final StateMachineSecurityInterceptor<S, E> securityInterceptor = new StateMachineSecurityInterceptor<S, E>(
@@ -338,28 +372,6 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
holderItem.value.setState(stateMap.get(holderItem.key));
}
// set parent machines for each built machine
for (Entry<Object, StateMachine<S, E>> mme : machineMap.entrySet()) {
StateMachine<S, E> m = null;
if (mme.getKey() != null) {
Object sParent = null;
for (StateData<S, E> sd : stateMachineModel.getStatesData().getStateData()) {
if (ObjectUtils.nullSafeEquals(sd.getState(), mme.getKey())) {
sParent = sd.getParent();
break;
}
}
m = machineMap.get(sParent);
}
final StateMachine<S, E> mm = m;
mme.getValue().getStateMachineAccessor().doWithRegion(new StateMachineFunction<StateMachineAccess<S ,E>>(){
@Override
public void apply(StateMachineAccess<S, E> function) {
function.setParentMachine(mm);
}
});
}
return delegateAutoStartup(machine);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2016 the original author or authors.
* Copyright 2015-2017 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.
@@ -76,6 +76,7 @@ public class ObjectStateMachineFactory<S, E> extends AbstractStateMachineFactory
extendedState, uuid);
machine.setId(machineId);
machine.setHistoryState(historyState);
machine.setTransitionConflightPolicy(stateMachineModel.getConfigurationData().getTransitionConflightPolicy());
if (contextEventsEnabled != null) {
machine.setContextEventsEnabled(contextEventsEnabled);
}
@@ -91,7 +92,6 @@ public class ObjectStateMachineFactory<S, E> extends AbstractStateMachineFactory
if (machine instanceof BeanNameAware) {
((BeanNameAware)machine).setBeanName(beanName);
}
machine.afterPropertiesSet();
return machine;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2017 the original author or authors.
* Copyright 2017 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.
@@ -46,6 +46,7 @@ import org.springframework.statemachine.monitor.StateMachineMonitor;
import org.springframework.statemachine.persist.StateMachineRuntimePersister;
import org.springframework.statemachine.security.SecurityRule;
import org.springframework.statemachine.support.StateMachineInterceptor;
import org.springframework.statemachine.transition.TransitionConflightPolicy;
/**
* {@link AnnotationBuilder} for {@link StatesData}.
@@ -64,6 +65,7 @@ public class StateMachineConfigurationBuilder<S, E>
private TaskExecutor taskExecutor;
private TaskScheduler taskScheculer;
private boolean autoStart = false;
private TransitionConflightPolicy transitionConflightPolicy;
private StateMachineEnsemble<S, E> ensemble;
private final List<StateMachineListener<S, E>> listeners = new ArrayList<StateMachineListener<S, E>>();
private boolean securityEnabled = false;
@@ -145,7 +147,7 @@ public class StateMachineConfigurationBuilder<S, E>
}
return new ConfigurationData<S, E>(beanFactory, taskExecutor, taskScheculer, autoStart, ensemble, listeners,
securityEnabled, transitionSecurityAccessDecisionManager, eventSecurityAccessDecisionManager, eventSecurityRule,
transitionSecurityRule, verifierEnabled, verifier, machineId, stateMachineMonitor, interceptorsCopy);
transitionSecurityRule, verifierEnabled, verifier, machineId, stateMachineMonitor, interceptorsCopy, transitionConflightPolicy);
}
/**
@@ -292,4 +294,13 @@ public class StateMachineConfigurationBuilder<S, E>
public void setStateMachineRuntimePersister(StateMachineRuntimePersister<S, E, ?> persister) {
this.persister = persister;
}
/**
* Sets the transition conflight policy.
*
* @param transitionConflightPolicy the new transition conflight policy
*/
public void setTransitionConflightPolicy(TransitionConflightPolicy transitionConflightPolicy) {
this.transitionConflightPolicy = transitionConflightPolicy;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-2017 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.
@@ -22,6 +22,7 @@ import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerBuilder;
import org.springframework.statemachine.listener.StateMachineListener;
import org.springframework.statemachine.transition.TransitionConflightPolicy;
/**
* Base {@code ConfigConfigurer} interface for configuring generic config.
@@ -86,4 +87,11 @@ public interface ConfigurationConfigurer<S, E> extends
*/
ConfigurationConfigurer<S, E> listener(StateMachineListener<S, E> listener);
/**
* Speficy a {@link TransitionConflightPolicy}.
*
* @param transitionConflightPolicy the transition conflight policy
* @return the configuration configurer
*/
ConfigurationConfigurer<S, E> transitionConflightPolicy(TransitionConflightPolicy transitionConflightPolicy);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2016 the original author or authors.
* Copyright 2015-2017 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.
@@ -26,6 +26,7 @@ import org.springframework.statemachine.config.builders.StateMachineConfiguratio
import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerAdapter;
import org.springframework.statemachine.config.model.ConfigurationData;
import org.springframework.statemachine.listener.StateMachineListener;
import org.springframework.statemachine.transition.TransitionConflightPolicy;
/**
* Default implementation of a {@link ConfigurationConfigurer}.
@@ -44,6 +45,7 @@ public class DefaultConfigurationConfigurer<S, E>
private TaskExecutor taskExecutor;
private TaskScheduler taskScheculer;
private boolean autoStart = false;
private TransitionConflightPolicy transitionConflightPolicy;
private final List<StateMachineListener<S, E>> listeners = new ArrayList<StateMachineListener<S, E>>();
@Override
@@ -54,6 +56,7 @@ public class DefaultConfigurationConfigurer<S, E>
builder.setTaskScheculer(taskScheculer);
builder.setAutoStart(autoStart);
builder.setStateMachineListeners(listeners);
builder.setTransitionConflightPolicy(transitionConflightPolicy);
}
@Override
@@ -92,4 +95,9 @@ public class DefaultConfigurationConfigurer<S, E>
return this;
}
@Override
public ConfigurationConfigurer<S, E> transitionConflightPolicy(TransitionConflightPolicy transitionConflightPolicy) {
this.transitionConflightPolicy = transitionConflightPolicy;
return this;
}
}

View File

@@ -32,6 +32,7 @@ import org.springframework.statemachine.listener.StateMachineListener;
import org.springframework.statemachine.monitor.StateMachineMonitor;
import org.springframework.statemachine.security.SecurityRule;
import org.springframework.statemachine.support.StateMachineInterceptor;
import org.springframework.statemachine.transition.TransitionConflightPolicy;
/**
* Configuration object used to keep things together in {@link StateMachineConfigurationBuilder}.
@@ -48,6 +49,7 @@ public class ConfigurationData<S, E> {
private final TaskExecutor taskExecutor;
private final TaskScheduler taskScheduler;
private final boolean autoStart;
private final TransitionConflightPolicy transitionConflightPolicy;
private final StateMachineEnsemble<S, E> ensemble;
private final List<StateMachineListener<S, E>> listeners;
private final boolean securityEnabled;
@@ -95,6 +97,39 @@ public class ConfigurationData<S, E> {
SecurityRule eventSecurityRule, SecurityRule transitionSecurityRule, boolean verifierEnabled,
StateMachineModelVerifier<S, E> verifier, String machineId, StateMachineMonitor<S, E> stateMachineMonitor,
List<StateMachineInterceptor<S, E>> interceptors) {
this(beanFactory, taskExecutor, taskScheduler, autoStart, ensemble, listeners, securityEnabled,
transitionSecurityAccessDecisionManager, eventSecurityAccessDecisionManager, eventSecurityRule, transitionSecurityRule,
verifierEnabled, verifier, machineId, stateMachineMonitor, interceptors, null);
}
/**
* Instantiates a new state machine configuration config data.
*
* @param beanFactory the bean factory
* @param taskExecutor the task executor
* @param taskScheduler the task scheduler
* @param autoStart the autostart flag
* @param ensemble the state machine ensemble
* @param listeners the state machine listeners
* @param securityEnabled the security enabled flag
* @param transitionSecurityAccessDecisionManager the transition security access decision manager
* @param eventSecurityAccessDecisionManager the event security access decision manager
* @param eventSecurityRule the event security rule
* @param transitionSecurityRule the transition security rule
* @param verifierEnabled the verifier enabled flag
* @param verifier the state machine model verifier
* @param machineId the machine id
* @param stateMachineMonitor the state machine monitor
* @param interceptors the state machine interceptors.
* @param transitionConflightPolicy the transition conflight policy
*/
public ConfigurationData(BeanFactory beanFactory, TaskExecutor taskExecutor,
TaskScheduler taskScheduler, boolean autoStart, StateMachineEnsemble<S, E> ensemble,
List<StateMachineListener<S, E>> listeners, boolean securityEnabled,
AccessDecisionManager transitionSecurityAccessDecisionManager, AccessDecisionManager eventSecurityAccessDecisionManager,
SecurityRule eventSecurityRule, SecurityRule transitionSecurityRule, boolean verifierEnabled,
StateMachineModelVerifier<S, E> verifier, String machineId, StateMachineMonitor<S, E> stateMachineMonitor,
List<StateMachineInterceptor<S, E>> interceptors, TransitionConflightPolicy transitionConflightPolicy) {
this.beanFactory = beanFactory;
this.taskExecutor = taskExecutor;
this.taskScheduler = taskScheduler;
@@ -111,6 +146,7 @@ public class ConfigurationData<S, E> {
this.machineId = machineId;
this.stateMachineMonitor = stateMachineMonitor;
this.interceptors = interceptors;
this.transitionConflightPolicy = transitionConflightPolicy;
}
public String getMachineId() {
@@ -251,4 +287,13 @@ public class ConfigurationData<S, E> {
public List<StateMachineInterceptor<S, E>> getStateMachineInterceptors() {
return interceptors;
}
/**
* Gets the transition conflight policy.
*
* @return the transition conflight policy
*/
public TransitionConflightPolicy getTransitionConflightPolicy() {
return transitionConflightPolicy;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2017 the original author or authors.
* Copyright 2017 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.
@@ -21,6 +21,7 @@ import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.context.Lifecycle;
import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
@@ -51,6 +52,7 @@ 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.TransitionConflightPolicy;
import org.springframework.statemachine.transition.TransitionKind;
import org.springframework.statemachine.trigger.DefaultTriggerContext;
import org.springframework.statemachine.trigger.Trigger;
@@ -91,6 +93,8 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
private ExtendedState extendedState;
private TransitionConflightPolicy transitionConflightPolicy;
private volatile State<S,E> currentState;
// using this to log last state when machine stops, as
@@ -274,12 +278,19 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
}
DefaultStateMachineExecutor<S, E> executor = new DefaultStateMachineExecutor<S, E>(this, getRelayStateMachine(), transitions,
triggerToTransitionMap, triggerlessTransitions, initialTransition, initialEvent);
triggerToTransitionMap, triggerlessTransitions, initialTransition, initialEvent, transitionConflightPolicy);
if (getBeanFactory() != null) {
executor.setBeanFactory(getBeanFactory());
}
if (getTaskExecutor() != null){
executor.setTaskExecutor(getTaskExecutor());
// parent machine is set when we're on substates(not regions)
// so then force sync executor which makes things a bit more reliable
// as state execution should anyway get synched with plain substates.
if(parentMachine != null) {
executor.setTaskExecutor(new SyncTaskExecutor());
} else {
executor.setTaskExecutor(getTaskExecutor());
}
}
executor.afterPropertiesSet();
executor.setStateMachineExecutorTransit(new StateMachineExecutorTransit<S, E>() {
@@ -562,6 +573,15 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
forwardedInitialEvent = message;
}
/**
* Sets the transition conflight policy.
*
* @param transitionConflightPolicy the new transition conflight policy
*/
public void setTransitionConflightPolicy(TransitionConflightPolicy transitionConflightPolicy) {
this.transitionConflightPolicy = transitionConflightPolicy;
}
private boolean sendEventInternal(Message<E> event) {
if (hasStateMachineError()) {
// TODO: should we throw exception?

View File

@@ -47,6 +47,7 @@ import org.springframework.statemachine.state.JoinPseudoState;
import org.springframework.statemachine.state.PseudoStateKind;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.transition.Transition;
import org.springframework.statemachine.transition.TransitionConflightPolicy;
import org.springframework.statemachine.trigger.DefaultTriggerContext;
import org.springframework.statemachine.trigger.TimerTrigger;
import org.springframework.statemachine.trigger.Trigger;
@@ -101,6 +102,8 @@ public class DefaultStateMachineExecutor<S, E> extends LifecycleObjectSupport im
private final ReentrantLock lock = new ReentrantLock();
private final TransitionComparator<S, E> transitionComparator;;
/**
* Instantiates a new default state machine executor.
*
@@ -111,10 +114,12 @@ public class DefaultStateMachineExecutor<S, E> extends LifecycleObjectSupport im
* @param triggerlessTransitions the triggerless transitions
* @param initialTransition the initial transition
* @param initialEvent the initial event
* @param transitionConflightPolicy the transition conflight policy
*/
public DefaultStateMachineExecutor(StateMachine<S, E> stateMachine, StateMachine<S, E> relayStateMachine,
Collection<Transition<S, E>> transitions, Map<Trigger<S, E>, Transition<S, E>> triggerToTransitionMap,
List<Transition<S, E>> triggerlessTransitions, Transition<S, E> initialTransition, Message<E> initialEvent) {
List<Transition<S, E>> triggerlessTransitions, Transition<S, E> initialTransition, Message<E> initialEvent,
TransitionConflightPolicy transitionConflightPolicy) {
this.stateMachine = stateMachine;
this.relayStateMachine = relayStateMachine;
this.triggerToTransitionMap = triggerToTransitionMap;
@@ -122,6 +127,9 @@ public class DefaultStateMachineExecutor<S, E> extends LifecycleObjectSupport im
this.transitions = transitions;
this.initialTransition = initialTransition;
this.initialEvent = initialEvent;
this.transitionComparator = new TransitionComparator<S, E>(transitionConflightPolicy);
// anonymous transitions are fixed, sort those now
this.triggerlessTransitions.sort(transitionComparator);
registerTriggerListener();
}
@@ -417,7 +425,8 @@ public class DefaultStateMachineExecutor<S, E> extends LifecycleObjectSupport im
trans.add(triggerToTransitionMap.get(queueItem.trigger));
}
// go through candidates and transit max one
// go through candidates and transit max one, sort before handling
trans.sort(transitionComparator);
handleTriggerTrans(trans, queuedMessage);
}
if (stateMachine.getState() != null) {

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2017 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.Comparator;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.statemachine.transition.Transition;
import org.springframework.statemachine.transition.TransitionConflightPolicy;
/**
* {@link Comparator} for {@link Transition}s. This comparator tries to compare
* transitions simply checking
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
class TransitionComparator<S, E> implements Comparator<Transition<S, E>> {
private final static Log log = LogFactory.getLog(TransitionComparator.class);
private final TransitionConflightPolicy transitionConflightPolicy;
/**
* Instantiates a new transition comparator.
*
* @param transitionConflightPolicy the transition conflight policy
*/
public TransitionComparator(TransitionConflightPolicy transitionConflightPolicy) {
this.transitionConflightPolicy = transitionConflightPolicy == null ? TransitionConflightPolicy.CHILD : transitionConflightPolicy;
}
@Override
public int compare(Transition<S, E> left, Transition<S, E> right) {
if (log.isTraceEnabled()) {
log.trace("Compare left='" + left + "' right='" + right +"'");
}
if (left == right) {
return 0;
} else {
boolean substate = StateMachineUtils.isSubstate(left.getSource(), right.getSource());
if (transitionConflightPolicy == TransitionConflightPolicy.CHILD) {
return substate ? 1 : -1;
} else {
return substate ? -1 : 1;
}
}
}
@Override
public String toString() {
return "TransitionComparator [transitionConflightPolicy=" + transitionConflightPolicy + "]";
}
}

View File

@@ -168,4 +168,9 @@ public abstract class AbstractTransition<S, E> implements Transition<S, E> {
}
}
}
@Override
public String toString() {
return "AbstractTransition [source=" + source + ", target=" + target + ", kind=" + kind + ", guard=" + guard + "]";
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2017 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.transition;
/**
* Enumerations for possible transition conflight policies.
*
* @author Janne Valkealahti
*
*/
public enum TransitionConflightPolicy {
/**
* Choose transition from a child.
*/
CHILD,
/**
* Choose transition from a parent.
*/
PARENT;
}

View File

@@ -89,7 +89,8 @@ public class DefaultStateMachineExecutorTests {
triggerToTransitionMap,
triggerlessTransitions,
initialTransition,
initialEvent);
initialEvent,
null);
executor.setTaskExecutor(taskExecutor);
@@ -162,7 +163,8 @@ public class DefaultStateMachineExecutorTests {
triggerToTransitionMap,
triggerlessTransitions,
initialTransition,
initialEvent);
initialEvent,
null);
executor.setTaskExecutor(taskExecutor);
@@ -237,7 +239,8 @@ public class DefaultStateMachineExecutorTests {
triggerToTransitionMap,
triggerlessTransitions,
initialTransition,
initialEvent);
initialEvent,
null);
executor.setTaskExecutor(taskExecutor);

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2017 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 static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.util.ArrayList;
import java.util.Collection;
import org.junit.Test;
import org.springframework.statemachine.AbstractStateMachineTests.TestEvents;
import org.springframework.statemachine.AbstractStateMachineTests.TestStates;
import org.springframework.statemachine.ObjectStateMachine;
import org.springframework.statemachine.state.DefaultPseudoState;
import org.springframework.statemachine.state.EnumState;
import org.springframework.statemachine.state.PseudoState;
import org.springframework.statemachine.state.PseudoStateKind;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.state.StateMachineState;
import org.springframework.statemachine.transition.DefaultExternalTransition;
import org.springframework.statemachine.transition.Transition;
import org.springframework.statemachine.transition.TransitionConflightPolicy;
import org.springframework.statemachine.trigger.EventTrigger;
/**
* Tests for {@link TransitionComparator}.
*
* @author Janne Valkealahti
*
*/
public class TransitionComparatorTests {
@Test
public void testCompareWithParentAndChild() {
PseudoState<TestStates, TestEvents> pseudoState = new DefaultPseudoState<TestStates, TestEvents>(PseudoStateKind.INITIAL);
State<TestStates, TestEvents> stateS111 = new EnumState<TestStates, TestEvents>(TestStates.S111, null, null, null, pseudoState);
// submachine 11
Collection<State<TestStates, TestEvents>> substates111 = new ArrayList<State<TestStates, TestEvents>>();
substates111.add(stateS111);
Collection<Transition<TestStates, TestEvents>> subtransitions111 = new ArrayList<Transition<TestStates, TestEvents>>();
ObjectStateMachine<TestStates, TestEvents> submachine11 = new ObjectStateMachine<TestStates, TestEvents>(substates111,
subtransitions111, stateS111);
// submachine 1
StateMachineState<TestStates, TestEvents> stateS11 = new StateMachineState<TestStates, TestEvents>(TestStates.S11, submachine11,
null, null, null, pseudoState);
Collection<State<TestStates, TestEvents>> substates11 = new ArrayList<State<TestStates, TestEvents>>();
substates11.add(stateS11);
Collection<Transition<TestStates, TestEvents>> subtransitions11 = new ArrayList<Transition<TestStates, TestEvents>>();
ObjectStateMachine<TestStates, TestEvents> submachine1 = new ObjectStateMachine<TestStates, TestEvents>(substates11,
subtransitions11, stateS11);
// machine
StateMachineState<TestStates, TestEvents> stateS1 = new StateMachineState<TestStates, TestEvents>(TestStates.S1, submachine1, null,
null, null, pseudoState);
DefaultExternalTransition<TestStates, TestEvents> transitionFromS111ToS1 = new DefaultExternalTransition<TestStates, TestEvents>(
stateS111, stateS1, null, TestEvents.E1, null, new EventTrigger<TestStates, TestEvents>(TestEvents.E1));
DefaultExternalTransition<TestStates, TestEvents> transitionFromS11ToS1 = new DefaultExternalTransition<TestStates, TestEvents>(
stateS11, stateS1, null, TestEvents.E1, null, new EventTrigger<TestStates, TestEvents>(TestEvents.E1));
TransitionComparator<TestStates, TestEvents> comparator = new TransitionComparator<>(null);
assertThat(comparator.compare(transitionFromS111ToS1, transitionFromS11ToS1), is(-1));
assertThat(comparator.compare(transitionFromS11ToS1, transitionFromS111ToS1), is(1));
assertThat(comparator.compare(transitionFromS111ToS1, transitionFromS111ToS1), is(0));
assertThat(comparator.compare(transitionFromS11ToS1, transitionFromS11ToS1), is(0));
comparator = new TransitionComparator<>(TransitionConflightPolicy.CHILD);
assertThat(comparator.compare(transitionFromS111ToS1, transitionFromS11ToS1), is(-1));
assertThat(comparator.compare(transitionFromS11ToS1, transitionFromS111ToS1), is(1));
assertThat(comparator.compare(transitionFromS111ToS1, transitionFromS111ToS1), is(0));
assertThat(comparator.compare(transitionFromS11ToS1, transitionFromS11ToS1), is(0));
comparator = new TransitionComparator<>(TransitionConflightPolicy.PARENT);
assertThat(comparator.compare(transitionFromS111ToS1, transitionFromS11ToS1), is(1));
assertThat(comparator.compare(transitionFromS11ToS1, transitionFromS111ToS1), is(-1));
assertThat(comparator.compare(transitionFromS111ToS1, transitionFromS111ToS1), is(0));
assertThat(comparator.compare(transitionFromS11ToS1, transitionFromS11ToS1), is(0));
}
}

View File

@@ -0,0 +1,620 @@
/*
* Copyright 2017 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.transition;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.assertThat;
import java.util.ArrayList;
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.messaging.support.MessageBuilder;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.statemachine.AbstractStateMachineTests;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.StateMachineSystemConstants;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
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;
/**
* Tests for transitions for cases where specific order is assumed.
*
* @author Janne Valkealahti
*
*/
public class TransitionOrderTests extends AbstractStateMachineTests {
@Override
protected AnnotationConfigApplicationContext buildContext() {
return new AnnotationConfigApplicationContext();
}
@SuppressWarnings("unchecked")
@Test
public void testAnonymousTransitionInConfigUseParent1() {
TestListener listener = new TestListener();
context.register(Config1.class);
context.refresh();
StateMachine<TestStates,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
machine.addStateListener(listener);
machine.start();
assertThat(machine.getState().getIds(), contains(TestStates.S1));
assertThat(listener.statesEntered, contains(TestStates.S1));
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build());
assertThat(listener.statesEntered, contains(TestStates.S1, TestStates.S10, TestStates.S1011, TestStates.S1));
}
@SuppressWarnings("unchecked")
@Test
public void testAnonymousTransitionInConfigUseParent2() {
TestListener listener = new TestListener();
context.register(Config2.class);
context.refresh();
StateMachine<TestStates,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
machine.addStateListener(listener);
machine.start();
assertThat(machine.getState().getIds(), contains(TestStates.S1));
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build());
assertThat(listener.statesEntered, contains(TestStates.S1, TestStates.S10, TestStates.S1011, TestStates.S1));
}
@SuppressWarnings("unchecked")
@Test
public void testAnonymousTransitionInConfigUseChild1() {
TestListener listener = new TestListener();
context.register(Config3.class);
context.refresh();
StateMachine<TestStates,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
machine.addStateListener(listener);
machine.start();
assertThat(machine.getState().getIds(), contains(TestStates.S1));
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build());
assertThat(listener.statesEntered, contains(TestStates.S1, TestStates.S10, TestStates.S1011, TestStates.S1012, TestStates.S1));
}
@SuppressWarnings("unchecked")
@Test
public void testAnonymousTransitionInConfigUseParent3() {
TestListener listener = new TestListener();
context.register(Config4.class);
context.refresh();
StateMachine<TestStates,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
machine.addStateListener(listener);
machine.start();
assertThat(machine.getState().getIds(), contains(TestStates.S1));
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build());
assertThat(listener.statesEntered, contains(TestStates.S1, TestStates.S10, TestStates.S1011, TestStates.S1));
}
@SuppressWarnings("unchecked")
@Test
public void testAnonymousTransitionInConfigUseParent4() {
TestListener listener = new TestListener();
context.register(Config5.class);
context.refresh();
StateMachine<TestStates,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
machine.addStateListener(listener);
machine.start();
assertThat(machine.getState().getIds(), contains(TestStates.S1));
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build());
assertThat(listener.statesEntered, contains(TestStates.S1, TestStates.S10, TestStates.S1011, TestStates.S1012));
}
@SuppressWarnings("unchecked")
@Test
public void testAnonymousTransitionInConfigUseParent5() {
TestListener listener = new TestListener();
context.register(Config6.class);
context.refresh();
StateMachine<TestStates,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
machine.addStateListener(listener);
machine.start();
assertThat(machine.getState().getIds(), contains(TestStates.S1));
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build());
assertThat(listener.statesEntered, contains(TestStates.S1, TestStates.S10, TestStates.S1011, TestStates.S1));
}
@SuppressWarnings("unchecked")
@Test
public void testAnonymousTransitionInConfigUseParent6() {
TestListener listener = new TestListener();
context.register(Config7.class);
context.refresh();
StateMachine<TestStates,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
machine.addStateListener(listener);
machine.start();
assertThat(machine.getState().getIds(), contains(TestStates.S1));
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build());
assertThat(listener.statesEntered,
contains(TestStates.S1, TestStates.S10, TestStates.S1011, TestStates.S1012, TestStates.S2011, TestStates.S1));
}
@SuppressWarnings("unchecked")
@Test
public void testAnonymousTransitionInConfigUseParent7() {
TestListener listener = new TestListener();
context.register(Config8.class);
context.refresh();
StateMachine<TestStates,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
machine.addStateListener(listener);
machine.start();
assertThat(machine.getState().getIds(), contains(TestStates.S1));
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build());
assertThat(listener.statesEntered,
contains(TestStates.S1, TestStates.S10, TestStates.S1011, TestStates.S1));
}
@SuppressWarnings("unchecked")
@Test
public void testAnonymousTransitionInConfigUseParent3Threading() throws InterruptedException {
TestListener listener = new TestListener();
context.register(Config4.class, StateMachineExecutorConfiguration.class);
context.refresh();
StateMachine<TestStates,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
machine.addStateListener(listener);
machine.start();
assertThat(listener.statesEnteredLatch.await(1, TimeUnit.SECONDS), is(true));
assertThat(machine.getState().getIds(), contains(TestStates.S1));
listener.reset(1, 3);
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build());
assertThat(listener.statesEnteredLatch.await(1, TimeUnit.SECONDS), is(true));
assertThat(listener.statesEntered, contains(TestStates.S10, TestStates.S1011, TestStates.S1));
}
@Configuration
@EnableStateMachine
public static class Config1 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@Override
public void configure(StateMachineConfigurationConfigurer<TestStates, TestEvents> config) throws Exception {
config
.withConfiguration()
.transitionConflightPolicy(TransitionConflightPolicy.PARENT);
}
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
states
.withStates()
.initial(TestStates.S1)
.state(TestStates.S10)
.and()
.withStates()
.parent(TestStates.S10)
.initial(TestStates.S1011)
.state(TestStates.S1012);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
transitions
.withExternal()
.source(TestStates.S1)
.target(TestStates.S10)
.event(TestEvents.E1)
.and()
// Config1 vs. Config2, next 2 transitions are defined in different order
.withExternal()
.source(TestStates.S1011)
.target(TestStates.S1012)
.and()
.withExternal()
.source(TestStates.S10)
.target(TestStates.S1);
}
}
@Configuration
@EnableStateMachine
public static class Config2 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@Override
public void configure(StateMachineConfigurationConfigurer<TestStates, TestEvents> config) throws Exception {
config
.withConfiguration()
.transitionConflightPolicy(TransitionConflightPolicy.PARENT);
}
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
states
.withStates()
.initial(TestStates.S1)
.state(TestStates.S10)
.and()
.withStates()
.parent(TestStates.S10)
.initial(TestStates.S1011)
.state(TestStates.S1012);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
transitions
.withExternal()
.source(TestStates.S1)
.target(TestStates.S10)
.event(TestEvents.E1)
.and()
// Config1 vs. Config2, next 2 transitions are defined in different order
.withExternal()
.source(TestStates.S10)
.target(TestStates.S1)
.and()
.withExternal()
.source(TestStates.S1011)
.target(TestStates.S1012);
}
}
@Configuration
@EnableStateMachine
public static class Config3 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@Override
public void configure(StateMachineConfigurationConfigurer<TestStates, TestEvents> config) throws Exception {
config
.withConfiguration()
.transitionConflightPolicy(TransitionConflightPolicy.CHILD);
}
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
states
.withStates()
.initial(TestStates.S1)
.state(TestStates.S10)
.and()
.withStates()
.parent(TestStates.S10)
.initial(TestStates.S1011)
.state(TestStates.S1012);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
transitions
.withExternal()
.source(TestStates.S1)
.target(TestStates.S10)
.event(TestEvents.E1)
.and()
.withExternal()
.source(TestStates.S10)
.target(TestStates.S1)
.and()
.withExternal()
.source(TestStates.S1011)
.target(TestStates.S1012);
}
}
@Configuration
@EnableStateMachine
public static class Config4 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@Override
public void configure(StateMachineConfigurationConfigurer<TestStates, TestEvents> config) throws Exception {
config
.withConfiguration()
.transitionConflightPolicy(TransitionConflightPolicy.PARENT);
}
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
states
.withStates()
.initial(TestStates.S1)
.state(TestStates.S10)
.and()
.withStates()
.parent(TestStates.S10)
.initial(TestStates.S1011)
.stateEntry(TestStates.S1011, c -> c.getExtendedState().getVariables().put("error", true))
.state(TestStates.S1012);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
transitions
.withExternal()
.source(TestStates.S1)
.target(TestStates.S10)
.event(TestEvents.E1)
.and()
.withExternal()
.source(TestStates.S10)
.target(TestStates.S1)
.guard(c -> c.getExtendedState().getVariables().containsKey("error"))
.and()
.withExternal()
.source(TestStates.S1011)
.target(TestStates.S1012);
}
}
@Configuration
@EnableStateMachine
public static class Config5 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@Override
public void configure(StateMachineConfigurationConfigurer<TestStates, TestEvents> config) throws Exception {
config
.withConfiguration()
.transitionConflightPolicy(TransitionConflightPolicy.PARENT);
}
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
states
.withStates()
.initial(TestStates.S1)
.state(TestStates.S10)
.and()
.withStates()
.parent(TestStates.S10)
.initial(TestStates.S1011)
.state(TestStates.S1012);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
transitions
.withExternal()
.source(TestStates.S1)
.target(TestStates.S10)
.event(TestEvents.E1)
.and()
.withExternal()
.source(TestStates.S10)
.target(TestStates.S1)
.guard(c -> c.getExtendedState().getVariables().containsKey("error"))
.and()
.withExternal()
.source(TestStates.S1011)
.target(TestStates.S1012);
}
}
@Configuration
@EnableStateMachine
public static class Config6 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@Override
public void configure(StateMachineConfigurationConfigurer<TestStates, TestEvents> config) throws Exception {
config
.withConfiguration()
.transitionConflightPolicy(TransitionConflightPolicy.PARENT);
}
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
states
.withStates()
.initial(TestStates.S1)
.state(TestStates.S10)
.and()
.withStates()
.parent(TestStates.S10)
.initial(TestStates.S1011)
.state(TestStates.S1012)
.state(TestStates.S2011);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
transitions
.withExternal()
.source(TestStates.S1)
.target(TestStates.S10)
.event(TestEvents.E1)
.and()
.withExternal()
.source(TestStates.S10)
.target(TestStates.S1)
.and()
.withExternal()
.source(TestStates.S1011)
.target(TestStates.S1012)
.and()
.withExternal()
.source(TestStates.S1012)
.target(TestStates.S2011);
}
}
@Configuration
@EnableStateMachine
public static class Config7 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@Override
public void configure(StateMachineConfigurationConfigurer<TestStates, TestEvents> config) throws Exception {
config
.withConfiguration()
.transitionConflightPolicy(TransitionConflightPolicy.CHILD);
}
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
states
.withStates()
.initial(TestStates.S1)
.state(TestStates.S10)
.and()
.withStates()
.parent(TestStates.S10)
.initial(TestStates.S1011)
.state(TestStates.S1012)
.state(TestStates.S2011);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
transitions
.withExternal()
.source(TestStates.S1)
.target(TestStates.S10)
.event(TestEvents.E1)
.and()
.withExternal()
.source(TestStates.S10)
.target(TestStates.S1)
.and()
.withExternal()
.source(TestStates.S1011)
.target(TestStates.S1012)
.and()
.withExternal()
.source(TestStates.S1012)
.target(TestStates.S2011);
}
}
@Configuration
@EnableStateMachine
public static class Config8 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@Override
public void configure(StateMachineConfigurationConfigurer<TestStates, TestEvents> config) throws Exception {
config
.withConfiguration()
.transitionConflightPolicy(TransitionConflightPolicy.PARENT);
}
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
states
.withStates()
.initial(TestStates.S1)
.state(TestStates.S10)
.junction(TestStates.SF)
.and()
.withStates()
.parent(TestStates.S10)
.initial(TestStates.S1011)
.state(TestStates.S1012)
.state(TestStates.S2011);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
transitions
.withJunction()
.source(TestStates.SF)
.last(TestStates.S1)
.and()
.withExternal()
.source(TestStates.S1)
.target(TestStates.S10)
.event(TestEvents.E1)
.and()
.withExternal()
.source(TestStates.S1011)
.target(TestStates.S1012)
.and()
.withExternal()
.source(TestStates.S10)
.target(TestStates.SF)
.and()
.withExternal()
.source(TestStates.S1012)
.target(TestStates.S2011);
}
}
@Configuration
public static class StateMachineExecutorConfiguration {
@Bean(name = StateMachineSystemConstants.TASK_EXECUTOR_BEAN_NAME)
public TaskExecutor stateMachineTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
return executor;
}
}
static class TestListener extends StateMachineListenerAdapter<TestStates, TestEvents> {
volatile CountDownLatch stateChangedLatch = new CountDownLatch(1);
volatile int stateChangedCount = 0;
volatile CountDownLatch statesEnteredLatch = new CountDownLatch(1);
final ArrayList<TestStates> statesEntered = new ArrayList<>();
@Override
public void stateChanged(State<TestStates, TestEvents> from, State<TestStates, TestEvents> to) {
stateChangedCount++;
stateChangedLatch.countDown();
}
@Override
public void stateEntered(State<TestStates, TestEvents> state) {
statesEntered.add(state.getId());
statesEnteredLatch.countDown();
}
public void reset(int c1) {
stateChangedLatch = new CountDownLatch(c1);
stateChangedCount = 0;
}
public void reset(int c1, int c2) {
stateChangedLatch = new CountDownLatch(c1);
stateChangedCount = 0;
statesEnteredLatch = new CountDownLatch(c2);
statesEntered.clear();
}
}
}