Add skeleton for tasks recipe
- Relates to #74 - Tasks recipe not yet fully working, need to get this work in to be able to work with other tickets. - Add childs to StateMachineContext - A lot of fixes to regions concept throughout a code base. Some of the concepts were really broken if there were a complex recursive set of regions, etc. - Add task executor/scheduler to StateMachineBuilder
This commit is contained in:
@@ -155,6 +155,10 @@ configure(recipeProjects()) {
|
||||
project('spring-statemachine-recipes-common') {
|
||||
dependencies {
|
||||
compile project(":spring-statemachine-core")
|
||||
testCompile "org.springframework:spring-test:$springVersion"
|
||||
testCompile "org.hamcrest:hamcrest-core:$hamcrestVersion"
|
||||
testCompile "org.hamcrest:hamcrest-library:$hamcrestVersion"
|
||||
testCompile "junit:junit:$junitVersion"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.statemachine;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -28,11 +29,11 @@ import java.util.Map;
|
||||
public interface StateMachineContext<S, E> {
|
||||
|
||||
/**
|
||||
* Gets the state machine.
|
||||
* Gets the child contexts if any.
|
||||
*
|
||||
* @return the state machine
|
||||
* @return the child contexts
|
||||
*/
|
||||
StateMachine<S, E> getStateMachine();
|
||||
List<StateMachineContext<S, E>> getChilds();
|
||||
|
||||
/**
|
||||
* Gets the state.
|
||||
|
||||
@@ -151,19 +151,20 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
|
||||
|
||||
if (initialCount > 1) {
|
||||
for (Collection<StateData<S, E>> regionStateDatas : regionsStateDatas) {
|
||||
machine = buildMachine(machineMap, stateMap, regionStateDatas, transitionsData, getBeanFactory(),
|
||||
contextEvents, defaultExtendedState, stateMachineTransitions, getTaskExecutor(),
|
||||
getTaskScheduler());
|
||||
machine = buildMachine(machineMap, stateMap, regionStateDatas, transitionsData, resolveBeanFactory(),
|
||||
contextEvents, defaultExtendedState, stateMachineTransitions, resolveTaskExecutor(),
|
||||
resolveTaskScheduler());
|
||||
regionStack.push(new MachineStackItem<S, E>(machine));
|
||||
}
|
||||
|
||||
Collection<Region<S, E>> regions = new ArrayList<Region<S, E>>();
|
||||
for (MachineStackItem<S, E> si : regionStack) {
|
||||
regions.add(si.machine);
|
||||
while (!regionStack.isEmpty()) {
|
||||
MachineStackItem<S, E> pop = regionStack.pop();
|
||||
regions.add(pop.machine);
|
||||
}
|
||||
S parent = (S)peek.getParent();
|
||||
RegionState<S, E> rstate = new RegionState<S, E>(parent, regions, null, null, null,
|
||||
new DefaultPseudoState<S, E>(PseudoStateKind.INITIAL));
|
||||
RegionState<S, E> rstate = buildRegionStateInternal(parent, regions, null, stateData != null ? stateData.getEntryActions() : null,
|
||||
stateData != null ? stateData.getExitActions() : null, new DefaultPseudoState<S, E>(PseudoStateKind.INITIAL));
|
||||
if (stateData != null) {
|
||||
stateMap.put(stateData.getState(), rstate);
|
||||
} else {
|
||||
@@ -172,13 +173,13 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
|
||||
states.add(rstate);
|
||||
Transition<S, E> initialTransition = new InitialTransition<S, E>(rstate);
|
||||
StateMachine<S, E> m = buildStateMachineInternal(states, new ArrayList<Transition<S, E>>(), rstate,
|
||||
initialTransition, null, defaultExtendedState, null, contextEvents, getBeanFactory(),
|
||||
getTaskExecutor(), getTaskScheduler());
|
||||
initialTransition, null, defaultExtendedState, null, contextEvents, resolveBeanFactory(),
|
||||
resolveTaskExecutor(), resolveTaskScheduler());
|
||||
machine = m;
|
||||
}
|
||||
} else {
|
||||
machine = buildMachine(machineMap, stateMap, stateDatas, transitionsData, getBeanFactory(),
|
||||
contextEvents, defaultExtendedState, stateMachineTransitions, getTaskExecutor(), getTaskScheduler());
|
||||
machine = buildMachine(machineMap, stateMap, stateDatas, transitionsData, resolveBeanFactory(),
|
||||
contextEvents, defaultExtendedState, stateMachineTransitions, resolveTaskExecutor(), resolveTaskScheduler());
|
||||
if (peek.isInitial() || (!peek.isInitial() && !machineMap.containsKey(peek.getParent()))) {
|
||||
machineMap.put(peek.getParent(), machine);
|
||||
}
|
||||
@@ -214,6 +215,30 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
|
||||
this.contextEvents = contextEvents;
|
||||
}
|
||||
|
||||
private BeanFactory resolveBeanFactory() {
|
||||
if (stateMachineConfigurationConfig.getBeanFactory() != null) {
|
||||
return stateMachineConfigurationConfig.getBeanFactory();
|
||||
} else {
|
||||
return getBeanFactory();
|
||||
}
|
||||
}
|
||||
|
||||
private TaskExecutor resolveTaskExecutor() {
|
||||
if (stateMachineConfigurationConfig.getTaskExecutor() != null) {
|
||||
return stateMachineConfigurationConfig.getTaskExecutor();
|
||||
} else {
|
||||
return getTaskExecutor();
|
||||
}
|
||||
}
|
||||
|
||||
private TaskScheduler resolveTaskScheduler() {
|
||||
if (stateMachineConfigurationConfig.getTaskScheduler() != null) {
|
||||
return stateMachineConfigurationConfig.getTaskScheduler();
|
||||
} else {
|
||||
return getTaskScheduler();
|
||||
}
|
||||
}
|
||||
|
||||
private int getInitialCount(Collection<StateData<S, E>> stateDatas) {
|
||||
int count = 0;
|
||||
for (StateData<S, E> stateData : stateDatas) {
|
||||
@@ -506,4 +531,8 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
|
||||
return iterator;
|
||||
}
|
||||
|
||||
protected abstract RegionState<S, E> buildRegionStateInternal(S id, Collection<Region<S, E>> regions, Collection<E> deferred,
|
||||
Collection<? extends Action<S, E>> entryActions, Collection<? extends Action<S, E>> exitActions,
|
||||
PseudoState<S, E> pseudoState);
|
||||
|
||||
}
|
||||
|
||||
@@ -21,15 +21,17 @@ import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.core.task.TaskExecutor;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.statemachine.ObjectStateMachine;
|
||||
import org.springframework.statemachine.ExtendedState;
|
||||
import org.springframework.statemachine.ObjectStateMachine;
|
||||
import org.springframework.statemachine.StateMachine;
|
||||
import org.springframework.statemachine.action.Action;
|
||||
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfig;
|
||||
import org.springframework.statemachine.config.builders.StateMachineStates;
|
||||
import org.springframework.statemachine.config.builders.StateMachineTransitions;
|
||||
import org.springframework.statemachine.region.Region;
|
||||
import org.springframework.statemachine.state.ObjectState;
|
||||
import org.springframework.statemachine.state.PseudoState;
|
||||
import org.springframework.statemachine.state.RegionState;
|
||||
import org.springframework.statemachine.state.State;
|
||||
import org.springframework.statemachine.transition.Transition;
|
||||
|
||||
@@ -80,4 +82,10 @@ public class ObjectStateMachineFactory<S, E> extends AbstractStateMachineFactory
|
||||
return new ObjectState<S, E>(id, deferred, entryActions, exitActions, pseudoState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RegionState<S, E> buildRegionStateInternal(S id, Collection<Region<S, E>> regions, Collection<E> deferred,
|
||||
Collection<? extends Action<S, E>> entryActions, Collection<? extends Action<S, E>> exitActions, PseudoState<S, E> pseudoState) {
|
||||
return new RegionState<S, E>(id, regions, deferred, entryActions, exitActions, pseudoState);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -121,8 +121,8 @@ public class StateMachineBuilder {
|
||||
if (stateMachineConfigurationConfig.getTaskExecutor() != null) {
|
||||
stateMachineFactory.setTaskExecutor(stateMachineConfigurationConfig.getTaskExecutor());
|
||||
}
|
||||
if (stateMachineConfigurationConfig.getTaskScheculer() != null) {
|
||||
stateMachineFactory.setTaskScheduler(stateMachineConfigurationConfig.getTaskScheculer());
|
||||
if (stateMachineConfigurationConfig.getTaskScheduler() != null) {
|
||||
stateMachineFactory.setTaskScheduler(stateMachineConfigurationConfig.getTaskScheduler());
|
||||
}
|
||||
|
||||
return stateMachineFactory.getStateMachine();
|
||||
|
||||
@@ -32,7 +32,7 @@ public class StateMachineConfigurationConfig<S, E> {
|
||||
|
||||
private final BeanFactory beanFactory;
|
||||
private final TaskExecutor taskExecutor;
|
||||
private final TaskScheduler taskScheculer;
|
||||
private final TaskScheduler taskScheduler;
|
||||
private final StateMachineEnsemble<S, E> ensemble;
|
||||
|
||||
/**
|
||||
@@ -40,14 +40,14 @@ public class StateMachineConfigurationConfig<S, E> {
|
||||
*
|
||||
* @param beanFactory the bean factory
|
||||
* @param taskExecutor the task executor
|
||||
* @param taskScheculer the task scheculer
|
||||
* @param taskScheduler the task scheduler
|
||||
* @param ensemble the state machine ensemble
|
||||
*/
|
||||
public StateMachineConfigurationConfig(BeanFactory beanFactory, TaskExecutor taskExecutor,
|
||||
TaskScheduler taskScheculer, StateMachineEnsemble<S, E> ensemble) {
|
||||
TaskScheduler taskScheduler, StateMachineEnsemble<S, E> ensemble) {
|
||||
this.beanFactory = beanFactory;
|
||||
this.taskExecutor = taskExecutor;
|
||||
this.taskScheculer = taskScheculer;
|
||||
this.taskScheduler = taskScheduler;
|
||||
this.ensemble = ensemble;
|
||||
}
|
||||
|
||||
@@ -70,12 +70,12 @@ public class StateMachineConfigurationConfig<S, E> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the task scheculer.
|
||||
* Gets the task scheduler.
|
||||
*
|
||||
* @return the task scheculer
|
||||
* @return the task scheduler
|
||||
*/
|
||||
public TaskScheduler getTaskScheculer() {
|
||||
return taskScheculer;
|
||||
public TaskScheduler getTaskScheduler() {
|
||||
return taskScheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.springframework.statemachine.config.configurers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.statemachine.config.builders.StateMachineTransitionBuilder;
|
||||
@@ -50,6 +51,12 @@ public class DefaultJoinTransitionConfigurer<S, E>
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JoinTransitionConfigurer<S, E> sources(Collection<S> sources) {
|
||||
this.sources.addAll(sources);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JoinTransitionConfigurer<S, E> target(S target) {
|
||||
this.target = target;
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.statemachine.config.configurers;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
||||
import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerBuilder;
|
||||
import org.springframework.statemachine.transition.Transition;
|
||||
@@ -39,6 +41,14 @@ public interface JoinTransitionConfigurer<S, E>
|
||||
*/
|
||||
JoinTransitionConfigurer<S, E> source(S source);
|
||||
|
||||
/**
|
||||
* Specify a source states {@code S} for this {@link Transition}.
|
||||
*
|
||||
* @param sources the sources
|
||||
* @return configurer for chaining
|
||||
*/
|
||||
JoinTransitionConfigurer<S, E> sources(Collection<S> sources);
|
||||
|
||||
/**
|
||||
* Specify a target state {@code S} for this {@link Transition}.
|
||||
*
|
||||
|
||||
@@ -160,7 +160,7 @@ public class DistributedStateMachine<S, E> extends LifecycleObjectSupport implem
|
||||
public void preStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition,
|
||||
StateMachine<S, E> stateMachine) {
|
||||
if (message != null && ObjectUtils.nullSafeEquals(uuid, message.getHeaders().get("uuid"))) {
|
||||
ensemble.setState(new DefaultStateMachineContext<S, E>(delegate, transition.getTarget()
|
||||
ensemble.setState(new DefaultStateMachineContext<S, E>(transition.getTarget()
|
||||
.getId(), message.getPayload(), message.getHeaders(), stateMachine.getExtendedState()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ public class RegionState<S, E> extends AbstractState<S, E> {
|
||||
* @param pseudoState the pseudo state
|
||||
*/
|
||||
public RegionState(S id, Collection<Region<S, E>> regions, Collection<E> deferred,
|
||||
Collection<Action<S, E>> entryActions, Collection<Action<S, E>> exitActions, PseudoState<S, E> pseudoState) {
|
||||
Collection<? extends Action<S, E>> entryActions, Collection<? extends Action<S, E>> exitActions, PseudoState<S, E> pseudoState) {
|
||||
super(id, deferred, entryActions, exitActions, pseudoState, regions);
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ public class RegionState<S, E> extends AbstractState<S, E> {
|
||||
* @param exitActions the exit actions
|
||||
*/
|
||||
public RegionState(S id, Collection<Region<S, E>> regions, Collection<E> deferred,
|
||||
Collection<Action<S, E>> entryActions, Collection<Action<S, E>> exitActions) {
|
||||
Collection<? extends Action<S, E>> entryActions, Collection<? extends Action<S, E>> exitActions) {
|
||||
super(id, deferred, entryActions, exitActions, null, regions);
|
||||
}
|
||||
|
||||
@@ -168,7 +168,9 @@ public class RegionState<S, E> extends AbstractState<S, E> {
|
||||
ArrayList<State<S, E>> states = new ArrayList<State<S, E>>();
|
||||
states.add(this);
|
||||
for (Region<S, E> r : getRegions()) {
|
||||
states.addAll(r.getStates());
|
||||
for (State<S, E> s : r.getStates()) {
|
||||
states.addAll(s.getStates());
|
||||
}
|
||||
}
|
||||
return states;
|
||||
}
|
||||
|
||||
@@ -136,10 +136,10 @@ public class StateMachineState<S, E> extends AbstractState<S, E> {
|
||||
// don't stop if it looks like we're coming back
|
||||
// stop would cause start with entry which would
|
||||
// enable default transition and state
|
||||
if (getSubmachine().getState() != null
|
||||
if (getSubmachine().getState() != null && context.getTransition() != null
|
||||
&& context.getTransition().getSource().getId() != getSubmachine().getState().getId()) {
|
||||
getSubmachine().stop();
|
||||
} else if (!StateMachineUtils.isSubstate(context.getTransition().getTarget(), context.getTransition()
|
||||
} else if (context.getTransition() != null && !StateMachineUtils.isSubstate(context.getTransition().getTarget(), context.getTransition()
|
||||
.getSource())) {
|
||||
getSubmachine().stop();
|
||||
}
|
||||
|
||||
@@ -404,8 +404,7 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
|
||||
}
|
||||
|
||||
protected boolean acceptEvent(Message<E> message) {
|
||||
|
||||
boolean accepted = currentState.sendEvent(message);
|
||||
boolean accepted = (currentState != null && currentState.sendEvent(message));
|
||||
if (accepted) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -15,10 +15,11 @@
|
||||
*/
|
||||
package org.springframework.statemachine.support;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.statemachine.ExtendedState;
|
||||
import org.springframework.statemachine.StateMachine;
|
||||
import org.springframework.statemachine.StateMachineContext;
|
||||
|
||||
/**
|
||||
@@ -31,7 +32,7 @@ import org.springframework.statemachine.StateMachineContext;
|
||||
*/
|
||||
public class DefaultStateMachineContext<S, E> implements StateMachineContext<S, E> {
|
||||
|
||||
private final StateMachine<S, E> stateMachine;
|
||||
private final List<StateMachineContext<S, E>> childs;
|
||||
private final S state;
|
||||
private final E event;
|
||||
private final Map<String, Object> eventHeaders;
|
||||
@@ -40,14 +41,26 @@ public class DefaultStateMachineContext<S, E> implements StateMachineContext<S,
|
||||
/**
|
||||
* Instantiates a new default state machine context.
|
||||
*
|
||||
* @param stateMachine the state machine
|
||||
* @param state the state
|
||||
* @param event the event
|
||||
* @param eventHeaders the event headers
|
||||
* @param extendedState the extended state
|
||||
*/
|
||||
public DefaultStateMachineContext(StateMachine<S, E> stateMachine, S state, E event, Map<String, Object> eventHeaders, ExtendedState extendedState) {
|
||||
this.stateMachine = stateMachine;
|
||||
public DefaultStateMachineContext(S state, E event, Map<String, Object> eventHeaders, ExtendedState extendedState) {
|
||||
this(new ArrayList<StateMachineContext<S, E>>(), state, event, eventHeaders, extendedState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new default state machine context.
|
||||
*
|
||||
* @param childs the child state machine contexts
|
||||
* @param state the state
|
||||
* @param event the event
|
||||
* @param eventHeaders the event headers
|
||||
* @param extendedState the extended state
|
||||
*/
|
||||
public DefaultStateMachineContext(List<StateMachineContext<S, E>> childs, S state, E event, Map<String, Object> eventHeaders, ExtendedState extendedState) {
|
||||
this.childs = childs;
|
||||
this.state = state;
|
||||
this.event = event;
|
||||
this.eventHeaders = eventHeaders;
|
||||
@@ -55,8 +68,8 @@ public class DefaultStateMachineContext<S, E> implements StateMachineContext<S,
|
||||
}
|
||||
|
||||
@Override
|
||||
public StateMachine<S, E> getStateMachine() {
|
||||
return stateMachine;
|
||||
public List<StateMachineContext<S, E>> getChilds() {
|
||||
return childs;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -113,12 +113,25 @@ public class TransitionTests extends AbstractStateMachineTests {
|
||||
public void testTriggerlessTransitionInRegionsDefinedInSubStates() throws Exception {
|
||||
context.register(BaseConfig.class, Config5.class);
|
||||
context.refresh();
|
||||
|
||||
TestAction testAction1 = context.getBean("testAction1", TestAction.class);
|
||||
TestAction testAction20 = context.getBean("testAction20", TestAction.class);
|
||||
TestAction testAction21 = context.getBean("testAction21", TestAction.class);
|
||||
|
||||
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
|
||||
ObjectStateMachine<TestStates,TestEvents> machine =
|
||||
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
|
||||
machine.start();
|
||||
assertThat(machine.getState().getIds(), contains(TestStates.S1));
|
||||
machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1).build());
|
||||
|
||||
assertThat(testAction1.onExecuteLatch.await(2, TimeUnit.SECONDS), is(true));
|
||||
assertThat(testAction1.stateContexts.size(), is(1));
|
||||
assertThat(testAction20.onExecuteLatch.await(2, TimeUnit.SECONDS), is(true));
|
||||
assertThat(testAction20.stateContexts.size(), is(1));
|
||||
assertThat(testAction21.onExecuteLatch.await(2, TimeUnit.SECONDS), is(true));
|
||||
assertThat(testAction21.stateContexts.size(), is(1));
|
||||
|
||||
assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S2, TestStates.S201, TestStates.S211));
|
||||
}
|
||||
|
||||
@@ -322,16 +335,34 @@ public class TransitionTests extends AbstractStateMachineTests {
|
||||
.source(TestStates.S1)
|
||||
.target(TestStates.S2)
|
||||
.event(TestEvents.E1)
|
||||
.action(testAction1())
|
||||
.and()
|
||||
.withExternal()
|
||||
.state(TestStates.S2)
|
||||
.source(TestStates.S20)
|
||||
.target(TestStates.S201)
|
||||
.action(testAction20())
|
||||
.and()
|
||||
.withExternal()
|
||||
.state(TestStates.S2)
|
||||
.source(TestStates.S21)
|
||||
.target(TestStates.S211);
|
||||
.target(TestStates.S211)
|
||||
.action(testAction21());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestAction testAction1() {
|
||||
return new TestAction();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestAction testAction20() {
|
||||
return new TestAction();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestAction testAction21() {
|
||||
return new TestAction();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.recipes.support;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.statemachine.StateContext;
|
||||
import org.springframework.statemachine.action.Action;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StopWatch;
|
||||
|
||||
/**
|
||||
* A {@link Action} which executes a {@link Runnable}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*
|
||||
*/
|
||||
public class RunnableAction implements Action<String, String> {
|
||||
|
||||
private static final Log log = LogFactory.getLog(RunnableAction.class);
|
||||
private final Runnable runnable;
|
||||
private final String id;
|
||||
|
||||
/**
|
||||
* Instantiates a new runnable action.
|
||||
*
|
||||
* @param runnable the runnable
|
||||
*/
|
||||
public RunnableAction(Runnable runnable) {
|
||||
this(runnable, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new runnable action.
|
||||
*
|
||||
* @param runnable the runnable
|
||||
* @param id the optional id for logging
|
||||
*/
|
||||
public RunnableAction(Runnable runnable, String id) {
|
||||
Assert.notNull(runnable, "Runnable must be set");
|
||||
this.runnable = runnable;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void execute(StateContext<String, String> context) {
|
||||
if (!shouldExecute(context)) {
|
||||
return;
|
||||
}
|
||||
StopWatch watch = new StopWatch();
|
||||
String logId = (id == null ? "" : (" id=" + id));
|
||||
log.info("Executing runnable" + logId);
|
||||
if (log.isDebugEnabled()) {
|
||||
watch.start();
|
||||
}
|
||||
try {
|
||||
runnable.run();
|
||||
onSuccess(context);
|
||||
} catch (Exception e) {
|
||||
onError(context, e);
|
||||
}
|
||||
if (log.isDebugEnabled()) {
|
||||
watch.stop();
|
||||
log.debug("Runnable execution took " + watch.getTotalTimeMillis() + " ms" + logId);
|
||||
}
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
protected boolean shouldExecute(StateContext<String, String> context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void onSuccess(StateContext<String, String> context) {
|
||||
}
|
||||
|
||||
protected void onError(StateContext<String, String> context, Exception e) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,561 @@
|
||||
/*
|
||||
* 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.recipes.tasks;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.core.task.TaskExecutor;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.statemachine.StateContext;
|
||||
import org.springframework.statemachine.StateMachine;
|
||||
import org.springframework.statemachine.StateMachineException;
|
||||
import org.springframework.statemachine.action.Action;
|
||||
import org.springframework.statemachine.config.StateMachineBuilder;
|
||||
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
|
||||
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
||||
import org.springframework.statemachine.ensemble.StateMachinePersist;
|
||||
import org.springframework.statemachine.guard.Guard;
|
||||
import org.springframework.statemachine.listener.AbstractCompositeListener;
|
||||
import org.springframework.statemachine.recipes.support.RunnableAction;
|
||||
import org.springframework.statemachine.support.tree.Tree;
|
||||
import org.springframework.statemachine.support.tree.Tree.Node;
|
||||
import org.springframework.statemachine.support.tree.TreeTraverser;
|
||||
|
||||
/**
|
||||
* {@code TasksHandler} is a recipe for executing {@link Runnable} tasks
|
||||
* using a state machine logic.
|
||||
*
|
||||
*
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*
|
||||
*/
|
||||
public class TasksHandler {
|
||||
|
||||
private final static Log log = LogFactory.getLog(TasksHandler.class);
|
||||
|
||||
public final static String STATE_READY = "READY";
|
||||
public final static String STATE_FORK = "FORK";
|
||||
public final static String STATE_TASKS = "TASKS";
|
||||
public final static String STATE_JOIN = "JOIN";
|
||||
public final static String STATE_CHOICE = "CHOICE";
|
||||
public final static String STATE_ERROR = "ERROR";
|
||||
public final static String STATE_AUTOMATIC = "AUTOMATIC";
|
||||
public final static String STATE_MANUAL = "MANUAL";
|
||||
|
||||
public final static String STATE_TASKS_PREFIX = "TASK_";
|
||||
public final static String STATE_TASKS_INITIAL_POSTFIX = "_INITIAL";
|
||||
|
||||
public final static String EVENT_RUN = "RUN";
|
||||
public final static String EVENT_FALLBACK = "FALLBACK";
|
||||
public final static String EVENT_CONTINUE = "CONTINUE";
|
||||
public final static String EVENT_FIX = "FIX";
|
||||
|
||||
private StateMachine<String, String> stateMachine;
|
||||
private final CompositeTasksListener listener = new CompositeTasksListener();
|
||||
|
||||
/**
|
||||
* Instantiates a new tasks handler. Intentionally private instantiation
|
||||
* meant to be called from builder.
|
||||
*
|
||||
* @param tasks the wrapped tasks
|
||||
*/
|
||||
private TasksHandler(List<TaskWrapper> tasks) {
|
||||
try {
|
||||
this.stateMachine = buildStateMachine(tasks);
|
||||
} catch (Exception e) {
|
||||
throw new StateMachineException("Error building state machine from tasks", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void runTasks() {
|
||||
stateMachine.sendEvent(EVENT_RUN);
|
||||
}
|
||||
|
||||
public void continueFromError() {
|
||||
stateMachine.sendEvent(EVENT_CONTINUE);
|
||||
}
|
||||
|
||||
public void fixCurrentProblems() {
|
||||
stateMachine.sendEvent(EVENT_FIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the tasks listener.
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
public void addTasksListener(TasksListener listener) {
|
||||
this.listener.register(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the tasks listener.
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
public void removeTasksListener(TasksListener listener) {
|
||||
this.listener.unregister(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the internal state machine used by executing tasks.
|
||||
*
|
||||
* @return the state machine
|
||||
*/
|
||||
public StateMachine<String, String> getStateMachine() {
|
||||
return stateMachine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a new instance of a {@link Builder} which is used to build
|
||||
* an instance of a {@code TasksHandler}.
|
||||
*
|
||||
* @return the tasks handler builder
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
private StateMachine<String, String> buildStateMachine(List<TaskWrapper> tasks) throws Exception {
|
||||
StateMachineBuilder.Builder<String, String> builder = StateMachineBuilder.builder();
|
||||
|
||||
builder.configureConfiguration().withConfiguration()
|
||||
.taskExecutor(taskExecutor());
|
||||
|
||||
StateMachineStateConfigurer<String, String> stateMachineStateConfigurer = builder.configureStates();
|
||||
StateMachineTransitionConfigurer<String, String> stateMachineTransitionConfigurer = builder.configureTransitions();
|
||||
|
||||
stateMachineStateConfigurer
|
||||
.withStates()
|
||||
.initial(STATE_READY)
|
||||
.fork(STATE_FORK)
|
||||
.state(STATE_TASKS, tasksEntryAction(), null)
|
||||
.join(STATE_JOIN)
|
||||
.choice(STATE_CHOICE)
|
||||
.state(STATE_ERROR);
|
||||
|
||||
stateMachineTransitionConfigurer
|
||||
.withExternal()
|
||||
.source(STATE_READY).target(STATE_FORK).event(EVENT_RUN)
|
||||
.and()
|
||||
.withFork()
|
||||
.source(STATE_FORK).target(STATE_TASKS);
|
||||
|
||||
Iterator<Node<TaskWrapper>> iterator = buildTasksIterator(tasks);
|
||||
String parent = null;
|
||||
Collection<String> joinStates = new ArrayList<String>();
|
||||
while (iterator.hasNext()) {
|
||||
Node<TaskWrapper> node = iterator.next();
|
||||
if (node.getData() == null) {
|
||||
break;
|
||||
}
|
||||
String initial = STATE_TASKS_PREFIX + node.getData().id.toString() + STATE_TASKS_INITIAL_POSTFIX;
|
||||
String task = STATE_TASKS_PREFIX + node.getData().id.toString();
|
||||
parent = node.getData().parent != null ? STATE_TASKS_PREFIX + node.getData().parent.toString() : STATE_TASKS;
|
||||
|
||||
stateMachineStateConfigurer
|
||||
.withStates()
|
||||
.parent(parent)
|
||||
.initial(initial)
|
||||
.state(task, runnableAction(node.getData().runnable, node.getData().id.toString()), null);
|
||||
|
||||
joinStates.add(task);
|
||||
|
||||
stateMachineTransitionConfigurer
|
||||
.withExternal()
|
||||
.source(initial)
|
||||
.target(task);
|
||||
}
|
||||
|
||||
stateMachineStateConfigurer
|
||||
.withStates()
|
||||
.parent(STATE_ERROR)
|
||||
.initial(STATE_AUTOMATIC)
|
||||
.state(STATE_AUTOMATIC, automaticAction(), null)
|
||||
.state(STATE_MANUAL);
|
||||
|
||||
stateMachineTransitionConfigurer
|
||||
.withJoin()
|
||||
.sources(joinStates)
|
||||
.target(STATE_JOIN)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(STATE_JOIN).target(STATE_CHOICE)
|
||||
.and()
|
||||
.withChoice()
|
||||
.source(STATE_CHOICE)
|
||||
.first(STATE_ERROR, tasksChoiceGuard())
|
||||
.last(STATE_READY)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(STATE_ERROR).target(STATE_READY)
|
||||
.event(EVENT_CONTINUE)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(STATE_AUTOMATIC).target(STATE_MANUAL)
|
||||
.event(EVENT_FALLBACK)
|
||||
.and()
|
||||
.withInternal()
|
||||
.source(STATE_MANUAL)
|
||||
.action(fixAction())
|
||||
.event(EVENT_FIX);
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static TaskExecutor taskExecutor() {
|
||||
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
|
||||
taskExecutor.afterPropertiesSet();
|
||||
taskExecutor.setCorePoolSize(5);
|
||||
return taskExecutor;
|
||||
}
|
||||
|
||||
private static Iterator<Node<TaskWrapper>> buildTasksIterator(List<TaskWrapper> tasks) {
|
||||
Tree<TaskWrapper> tree = new Tree<TaskWrapper>();
|
||||
for (TaskWrapper wrapper : tasks) {
|
||||
tree.add(wrapper, wrapper.id, wrapper.parent);
|
||||
}
|
||||
|
||||
TreeTraverser<Node<TaskWrapper>> traverser = new TreeTraverser<Node<TaskWrapper>>() {
|
||||
@Override
|
||||
public Iterable<Node<TaskWrapper>> children(Node<TaskWrapper> root) {
|
||||
return root.getChildren();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Iterable<Node<TaskWrapper>> postOrderTraversal = traverser.postOrderTraversal(tree.getRoot());
|
||||
Iterator<Node<TaskWrapper>> iterator = postOrderTraversal.iterator();
|
||||
return iterator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder pattern implementation building a {@link TasksHandler}.
|
||||
*/
|
||||
public static class Builder {
|
||||
|
||||
private final List<TaskWrapper> tasks = new ArrayList<TaskWrapper>();
|
||||
|
||||
/**
|
||||
* Define a top-level task.
|
||||
*
|
||||
* @param id the id
|
||||
* @param runnable the runnable
|
||||
* @return the builder
|
||||
*/
|
||||
public Builder task(Object id, Runnable runnable) {
|
||||
tasks.add(new TaskWrapper(null, id, runnable));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a sub-task with a reference to its parent.
|
||||
*
|
||||
* @param parent the parent
|
||||
* @param id the id
|
||||
* @param runnable the runnable
|
||||
* @return the builder
|
||||
*/
|
||||
public Builder task(Object parent, Object id, Runnable runnable) {
|
||||
tasks.add(new TaskWrapper(parent, id, runnable));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a {@link StateMachinePersist} implementation if state machine
|
||||
* should be persisted with state changes.
|
||||
*
|
||||
* @param persist the persist
|
||||
* @return the builder
|
||||
*/
|
||||
public Builder persist(StateMachinePersist<String, String, Void> persist) {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the {@link TasksHandler}.
|
||||
*
|
||||
* @return the tasks handler
|
||||
*/
|
||||
public TasksHandler build() {
|
||||
return new TasksHandler(tasks);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private TasksEntryAction tasksEntryAction() {
|
||||
return new TasksEntryAction();
|
||||
}
|
||||
|
||||
private static LocalRunnableAction runnableAction(Runnable runnable, String id) {
|
||||
return new LocalRunnableAction(runnable, id);
|
||||
}
|
||||
|
||||
private static Guard<String, String> tasksChoiceGuard() {
|
||||
return new Guard<String, String>() {
|
||||
|
||||
@Override
|
||||
public boolean evaluate(StateContext<String, String> context) {
|
||||
Map<Object, Object> variables = context.getExtendedState().getVariables();
|
||||
for (Entry<Object, Object> entry : variables.entrySet()) {
|
||||
if (entry.getKey() instanceof String && ((String)entry.getKey()).startsWith(STATE_TASKS_PREFIX)) {
|
||||
if (entry.getValue() instanceof Integer) {
|
||||
Integer value = (Integer) entry.getValue();
|
||||
if (value < 0) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Task id=[" + entry.getKey() + "] has negative execution value, tasksChoiceGuard returns true");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Action<String, String> automaticAction() {
|
||||
return new Action<String, String>() {
|
||||
|
||||
@Override
|
||||
public void execute(StateContext<String, String> context) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Action<String, String> fixAction() {
|
||||
return new Action<String, String>() {
|
||||
|
||||
@Override
|
||||
public void execute(StateContext<String, String> context) {
|
||||
Map<Object, Object> variables = context.getExtendedState().getVariables();
|
||||
for (Entry<Object, Object> entry : variables.entrySet()) {
|
||||
if (entry.getKey() instanceof String && ((String)entry.getKey()).startsWith(STATE_TASKS_PREFIX)) {
|
||||
if (entry.getValue() instanceof Integer) {
|
||||
Integer value = (Integer) entry.getValue();
|
||||
if (value < 0) {
|
||||
variables.put(entry.getValue(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code TasksListener} is a generic interface listening tasks
|
||||
* execution events.
|
||||
*/
|
||||
public interface TasksListener {
|
||||
|
||||
/**
|
||||
* Called when all DAGs have either never executed or previous
|
||||
* execution was fully successful.
|
||||
*/
|
||||
void onTasksStarted();
|
||||
|
||||
/**
|
||||
* Called when some of a tasks in DAGs failed to execute and tasks
|
||||
* execution in going to continue.
|
||||
*/
|
||||
void onTasksContinue();
|
||||
|
||||
/**
|
||||
* Called before tasks is about to be executed.
|
||||
*
|
||||
* @param id the task id
|
||||
*/
|
||||
void onTaskPreExecute(Object id);
|
||||
|
||||
/**
|
||||
* Called after tasks has been executed regardless if task
|
||||
* execution succeeded or not.
|
||||
*
|
||||
* @param id the task id
|
||||
*/
|
||||
void onTaskPostExecute(Object id);
|
||||
|
||||
/**
|
||||
* Called when task execution resulter an error of any kind.
|
||||
*
|
||||
* @param id the task id
|
||||
* @param exception the exception
|
||||
*/
|
||||
void onTaskFailed(Object id, Exception exception);
|
||||
|
||||
/**
|
||||
* Called when all tasks has been executed successfully.
|
||||
*/
|
||||
void onTasksSuccess();
|
||||
|
||||
/**
|
||||
* Called when after an execution of full DAGs if some of the
|
||||
* tasks executed with an error.
|
||||
*/
|
||||
void onTasksError();
|
||||
}
|
||||
|
||||
private class CompositeTasksListener extends AbstractCompositeListener<TasksListener> implements
|
||||
TasksListener {
|
||||
|
||||
@Override
|
||||
public void onTasksStarted() {
|
||||
for (Iterator<TasksListener> iterator = getListeners().reverse(); iterator.hasNext();) {
|
||||
iterator.next().onTasksStarted();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTasksContinue() {
|
||||
for (Iterator<TasksListener> iterator = getListeners().reverse(); iterator.hasNext();) {
|
||||
iterator.next().onTasksContinue();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskPreExecute(Object id) {
|
||||
for (Iterator<TasksListener> iterator = getListeners().reverse(); iterator.hasNext();) {
|
||||
iterator.next().onTaskPreExecute(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskPostExecute(Object id) {
|
||||
for (Iterator<TasksListener> iterator = getListeners().reverse(); iterator.hasNext();) {
|
||||
iterator.next().onTaskPostExecute(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskFailed(Object id, Exception exception) {
|
||||
for (Iterator<TasksListener> iterator = getListeners().reverse(); iterator.hasNext();) {
|
||||
iterator.next().onTaskFailed(id, exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTasksSuccess() {
|
||||
for (Iterator<TasksListener> iterator = getListeners().reverse(); iterator.hasNext();) {
|
||||
iterator.next().onTasksSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTasksError() {
|
||||
for (Iterator<TasksListener> iterator = getListeners().reverse(); iterator.hasNext();) {
|
||||
iterator.next().onTasksError();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Action} which is executed when TASKS state is entered.
|
||||
*/
|
||||
private class TasksEntryAction implements Action<String, String> {
|
||||
|
||||
@Override
|
||||
public void execute(StateContext<String, String> context) {
|
||||
boolean hasErrors = false;
|
||||
Map<Object, Object> variables = context.getExtendedState().getVariables();
|
||||
for (Entry<Object, Object> entry : variables.entrySet()) {
|
||||
if (entry.getKey() instanceof String && ((String)entry.getKey()).startsWith(STATE_TASKS_PREFIX)) {
|
||||
if (entry.getValue() instanceof Integer) {
|
||||
Integer value = (Integer) entry.getValue();
|
||||
if (value < 0) {
|
||||
hasErrors = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasErrors) {
|
||||
listener.onTasksContinue();
|
||||
} else {
|
||||
listener.onTasksStarted();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Action} which is execution with every registered {@link Runnable}.
|
||||
*/
|
||||
private static class LocalRunnableAction extends RunnableAction {
|
||||
|
||||
public LocalRunnableAction(Runnable runnable, String id) {
|
||||
super(runnable, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldExecute(StateContext<String, String> context) {
|
||||
return super.shouldExecute(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSuccess(StateContext<String, String> context) {
|
||||
changeCount(1, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onError(StateContext<String, String> context, Exception e) {
|
||||
changeCount(-1, context);
|
||||
}
|
||||
|
||||
private void changeCount(int delta, StateContext<String, String> context) {
|
||||
Map<Object, Object> variables = context.getExtendedState().getVariables();
|
||||
Integer count;
|
||||
String key = STATE_TASKS_PREFIX + getId();
|
||||
if (variables.containsKey(key)) {
|
||||
count = (Integer) variables.get(key);
|
||||
} else {
|
||||
count = 0;
|
||||
}
|
||||
count =+ delta;
|
||||
variables.put(key, count);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapping a {@link Runnable} with a task identifier and parent if task
|
||||
* is a subtask. If parent is null it indicates that a task is a top-level
|
||||
* task with optional child tasks creating a dag task graph.
|
||||
*/
|
||||
private static class TaskWrapper {
|
||||
final Object parent;
|
||||
final Object id;
|
||||
final Runnable runnable;
|
||||
|
||||
public TaskWrapper(Object parent, Object id, Runnable runnable) {
|
||||
this.parent = parent;
|
||||
this.id = id;
|
||||
this.runnable = runnable;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
/*
|
||||
* 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.recipes;
|
||||
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.statemachine.StateMachine;
|
||||
import org.springframework.statemachine.listener.StateMachineListenerAdapter;
|
||||
import org.springframework.statemachine.recipes.tasks.TasksHandler;
|
||||
import org.springframework.statemachine.recipes.tasks.TasksHandler.TasksListener;
|
||||
import org.springframework.statemachine.state.State;
|
||||
import org.springframework.statemachine.transition.Transition;
|
||||
|
||||
public class TasksHandlerTests {
|
||||
|
||||
@Test
|
||||
public void testRunOnceSimpleNoFailures() throws InterruptedException {
|
||||
TasksHandler handler = TasksHandler.builder()
|
||||
.task("1", sleepRunnable())
|
||||
.task("2", sleepRunnable())
|
||||
.task("3", sleepRunnable())
|
||||
.build();
|
||||
|
||||
TestListener listener = new TestListener();
|
||||
listener.reset(10, 0, 0);
|
||||
StateMachine<String, String> machine = handler.getStateMachine();
|
||||
machine.addStateListener(listener);
|
||||
machine.start();
|
||||
assertThat(listener.stateMachineStartedLatch.await(1, TimeUnit.SECONDS), is(true));
|
||||
|
||||
handler.runTasks();
|
||||
|
||||
assertThat(listener.stateChangedLatch.await(8, TimeUnit.SECONDS), is(true));
|
||||
assertThat(listener.stateChangedCount, is(10));
|
||||
assertThat(machine.getState().getIds(), contains(TasksHandler.STATE_READY));
|
||||
Map<Object, Object> variables = machine.getExtendedState().getVariables();
|
||||
assertThat(variables.size(), is(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRunFailAndContinue() throws InterruptedException {
|
||||
TasksHandler handler = TasksHandler.builder()
|
||||
.task("1", sleepRunnable())
|
||||
.task("2", sleepRunnable())
|
||||
.task("3", failRunnable())
|
||||
.build();
|
||||
|
||||
TestListener listener = new TestListener();
|
||||
listener.reset(11, 0, 0);
|
||||
StateMachine<String, String> machine = handler.getStateMachine();
|
||||
machine.addStateListener(listener);
|
||||
machine.start();
|
||||
assertThat(listener.stateMachineStartedLatch.await(1, TimeUnit.SECONDS), is(true));
|
||||
|
||||
handler.runTasks();
|
||||
|
||||
assertThat(listener.stateChangedLatch.await(8, TimeUnit.SECONDS), is(true));
|
||||
assertThat(listener.stateChangedCount, is(11));
|
||||
assertThat(machine.getState().getIds(), contains(TasksHandler.STATE_ERROR, TasksHandler.STATE_AUTOMATIC));
|
||||
Map<Object, Object> variables = machine.getExtendedState().getVariables();
|
||||
assertThat(variables.size(), is(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDagSingleRoot() throws InterruptedException {
|
||||
TasksHandler handler = TasksHandler.builder()
|
||||
.task("1", sleepRunnable())
|
||||
.task("1", "12", sleepRunnable())
|
||||
.task("1", "13", sleepRunnable())
|
||||
.build();
|
||||
|
||||
TestListener listener = new TestListener();
|
||||
listener.reset(10, 0, 0);
|
||||
StateMachine<String, String> machine = handler.getStateMachine();
|
||||
machine.addStateListener(listener);
|
||||
machine.start();
|
||||
assertThat(listener.stateMachineStartedLatch.await(1, TimeUnit.SECONDS), is(true));
|
||||
|
||||
handler.runTasks();
|
||||
|
||||
assertThat(listener.stateChangedLatch.await(12, TimeUnit.SECONDS), is(true));
|
||||
assertThat(listener.stateChangedCount, is(10));
|
||||
assertThat(machine.getState().getIds(), contains(TasksHandler.STATE_READY));
|
||||
Map<Object, Object> variables = machine.getExtendedState().getVariables();
|
||||
assertThat(variables.size(), is(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDagMultiRoot() throws InterruptedException {
|
||||
TasksHandler handler = TasksHandler.builder()
|
||||
.task("1", sleepRunnable())
|
||||
.task("1", "12", sleepRunnable())
|
||||
.task("1", "13", sleepRunnable())
|
||||
.task("2", sleepRunnable())
|
||||
.task("2", "22", sleepRunnable())
|
||||
.task("2", "23", sleepRunnable())
|
||||
.task("3", sleepRunnable())
|
||||
.task("3", "32", sleepRunnable())
|
||||
.task("3", "33", sleepRunnable())
|
||||
.build();
|
||||
|
||||
TestListener listener = new TestListener();
|
||||
listener.reset(22, 0, 0);
|
||||
StateMachine<String, String> machine = handler.getStateMachine();
|
||||
|
||||
machine.addStateListener(listener);
|
||||
machine.start();
|
||||
assertThat(listener.stateMachineStartedLatch.await(1, TimeUnit.SECONDS), is(true));
|
||||
|
||||
handler.runTasks();
|
||||
|
||||
assertThat(listener.stateChangedLatch.await(20, TimeUnit.SECONDS), is(true));
|
||||
assertThat(listener.stateChangedCount, is(22));
|
||||
assertThat(machine.getState().getIds(), contains(TasksHandler.STATE_READY));
|
||||
Map<Object, Object> variables = machine.getExtendedState().getVariables();
|
||||
assertThat(variables.size(), is(9));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEvents() throws InterruptedException {
|
||||
TestTasksListener tasksListener = new TestTasksListener();
|
||||
|
||||
TasksHandler handler = TasksHandler.builder()
|
||||
.task("1", sleepRunnable())
|
||||
.task("2", sleepRunnable())
|
||||
.task("3", sleepRunnable())
|
||||
.build();
|
||||
|
||||
handler.addTasksListener(tasksListener);
|
||||
|
||||
TestListener listener = new TestListener();
|
||||
listener.reset(10, 0, 0);
|
||||
StateMachine<String, String> machine = handler.getStateMachine();
|
||||
machine.addStateListener(listener);
|
||||
machine.start();
|
||||
assertThat(listener.stateMachineStartedLatch.await(1, TimeUnit.SECONDS), is(true));
|
||||
|
||||
handler.runTasks();
|
||||
|
||||
assertThat(listener.stateChangedLatch.await(8, TimeUnit.SECONDS), is(true));
|
||||
assertThat(listener.stateChangedCount, is(10));
|
||||
assertThat(machine.getState().getIds(), contains(TasksHandler.STATE_READY));
|
||||
Map<Object, Object> variables = machine.getExtendedState().getVariables();
|
||||
assertThat(variables.size(), is(3));
|
||||
|
||||
assertThat(tasksListener.onTasksStartedLatch.await(1, TimeUnit.SECONDS), is(true));
|
||||
assertThat(tasksListener.onTasksStarted, is(1));
|
||||
}
|
||||
|
||||
private static Runnable sleepRunnable() {
|
||||
return new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Thread.sleep(2000);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Runnable failRunnable() {
|
||||
return new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static class TestListener extends StateMachineListenerAdapter<String, String> {
|
||||
|
||||
volatile CountDownLatch stateMachineStartedLatch = new CountDownLatch(1);
|
||||
volatile CountDownLatch stateChangedLatch = new CountDownLatch(1);
|
||||
volatile CountDownLatch stateEnteredLatch = new CountDownLatch(2);
|
||||
volatile CountDownLatch stateExitedLatch = new CountDownLatch(0);
|
||||
volatile CountDownLatch transitionLatch = new CountDownLatch(0);
|
||||
volatile int stateChangedCount = 0;
|
||||
volatile int transitionCount = 0;
|
||||
List<State<String, String>> statesEntered = new ArrayList<State<String, String>>();
|
||||
List<State<String, String>> statesExited = new ArrayList<State<String, String>>();
|
||||
|
||||
@Override
|
||||
public void stateMachineStarted(StateMachine<String, String> stateMachine) {
|
||||
stateMachineStartedLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stateChanged(State<String, String> from, State<String, String> to) {
|
||||
stateChangedCount++;
|
||||
stateChangedLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stateEntered(State<String, String> state) {
|
||||
statesEntered.add(state);
|
||||
stateEnteredLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stateExited(State<String, String> state) {
|
||||
statesExited.add(state);
|
||||
stateExitedLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transitionEnded(Transition<String, String> transition) {
|
||||
transitionCount++;
|
||||
transitionLatch.countDown();
|
||||
}
|
||||
|
||||
public void reset(int c1, int c2, int c3) {
|
||||
reset(c1, c2, c3, 0);
|
||||
}
|
||||
|
||||
public void reset(int c1, int c2, int c3, int c4) {
|
||||
stateChangedLatch = new CountDownLatch(c1);
|
||||
stateEnteredLatch = new CountDownLatch(c2);
|
||||
stateExitedLatch = new CountDownLatch(c3);
|
||||
transitionLatch = new CountDownLatch(c4);
|
||||
stateChangedCount = 0;
|
||||
transitionCount = 0;
|
||||
statesEntered.clear();
|
||||
statesExited.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class TestTasksListener implements TasksListener {
|
||||
|
||||
volatile CountDownLatch onTasksStartedLatch = new CountDownLatch(1);
|
||||
volatile CountDownLatch onTasksContinueLatch = new CountDownLatch(1);
|
||||
volatile CountDownLatch onTaskPreExecuteLatch = new CountDownLatch(1);
|
||||
volatile CountDownLatch onTaskPostExecuteLatch = new CountDownLatch(1);
|
||||
volatile CountDownLatch onTaskFailedLatch = new CountDownLatch(1);
|
||||
volatile CountDownLatch onTasksSuccessLatch = new CountDownLatch(1);
|
||||
volatile CountDownLatch onTasksErrorLatch = new CountDownLatch(1);
|
||||
|
||||
volatile int onTasksStarted;
|
||||
|
||||
@Override
|
||||
public void onTasksStarted() {
|
||||
onTasksStarted++;
|
||||
onTasksStartedLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTasksContinue() {
|
||||
onTasksContinueLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskPreExecute(Object id) {
|
||||
onTaskPreExecuteLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskPostExecute(Object id) {
|
||||
onTaskPostExecuteLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskFailed(Object id, Exception exception) {
|
||||
onTaskFailedLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTasksSuccess() {
|
||||
onTasksSuccessLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTasksError() {
|
||||
onTasksErrorLatch.countDown();
|
||||
}
|
||||
|
||||
public void reset(int c1, int c2, int c3, int c4, int c5, int c6, int c7) {
|
||||
onTasksStartedLatch = new CountDownLatch(c1);
|
||||
onTasksContinueLatch = new CountDownLatch(c2);
|
||||
onTaskPreExecuteLatch = new CountDownLatch(c3);
|
||||
onTaskPostExecuteLatch = new CountDownLatch(c4);
|
||||
onTaskFailedLatch = new CountDownLatch(c5);
|
||||
onTasksSuccessLatch = new CountDownLatch(c6);
|
||||
onTasksErrorLatch = new CountDownLatch(c7);
|
||||
onTasksStarted = 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
log4j.rootCategory=INFO, stdout
|
||||
|
||||
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
|
||||
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
|
||||
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %t %c{2} [%t] - %m%n
|
||||
|
||||
log4j.category.org.springframework.statemachine=DEBUG
|
||||
|
||||
@@ -125,7 +125,7 @@ public class ZookeeperStateMachinePersist<S, E> implements StateMachinePersist<S
|
||||
public StateMachineContext<S, E> read(Kryo kryo, Input input, Class<StateMachineContext<S, E>> clazz) {
|
||||
E event = (E) kryo.readClassAndObject(input);
|
||||
S state = (S) kryo.readClassAndObject(input);
|
||||
return new DefaultStateMachineContext<S, E>(null, state, event, null, null);
|
||||
return new DefaultStateMachineContext<S, E>(state, event, null, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -73,8 +73,8 @@ public class ZookeeperStateMachineEnsembleTests extends AbstractZookeeperTests {
|
||||
|
||||
assertThat(curatorClient.checkExists().forPath("/foo/data/current"), notNullValue());
|
||||
|
||||
ensemble.setState(new DefaultStateMachineContext<String, String>(null, "S1","E1", null, null));
|
||||
ensemble.setState(new DefaultStateMachineContext<String, String>(null, "S2","E1", null, null));
|
||||
ensemble.setState(new DefaultStateMachineContext<String, String>("S1","E1", null, null));
|
||||
ensemble.setState(new DefaultStateMachineContext<String, String>("S2","E1", null, null));
|
||||
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ public class ZookeeperStateMachineEnsembleTests extends AbstractZookeeperTests {
|
||||
assertThat(listener1.joinedLatch.await(2, TimeUnit.SECONDS), is(true));
|
||||
assertThat(listener2.joinedLatch.await(2, TimeUnit.SECONDS), is(true));
|
||||
|
||||
ensemble1.setState(new DefaultStateMachineContext<String, String>(stateMachine1, "S1", "E1", null, null));
|
||||
ensemble1.setState(new DefaultStateMachineContext<String, String>("S1", "E1", null, null));
|
||||
assertThat(listener2.eventLatch.await(2, TimeUnit.SECONDS), is(true));
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ public class ZookeeperStateMachinePersistTests extends AbstractZookeeperTests {
|
||||
curatorClient, "/KryoStateMachinePersistTests");
|
||||
|
||||
StateMachineContext<String, String> contextOut =
|
||||
new DefaultStateMachineContext<String, String>(null, "S1", "E1", null, null);
|
||||
new DefaultStateMachineContext<String, String>("S1", "E1", null, null);
|
||||
persist.write(contextOut, new Stat());
|
||||
StateMachineContext<String, String> contextIn = persist.read(new Stat());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user