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:
Janne Valkealahti
2015-07-03 14:02:17 +01:00
parent 44d09f257e
commit 4b858b1d9b
21 changed files with 1128 additions and 46 deletions

View File

@@ -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"
}
}

View File

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

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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();

View File

@@ -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;
}
/**

View File

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

View File

@@ -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}.
*

View File

@@ -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()));
}
}

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

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

View File

@@ -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();
}
}

View File

@@ -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) {
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

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

View File

@@ -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);
}
}

View File

@@ -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));
}

View File

@@ -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());