diff --git a/docs/src/reference/asciidoc/sm.adoc b/docs/src/reference/asciidoc/sm.adoc index c8e205a9..c0fcd1d7 100644 --- a/docs/src/reference/asciidoc/sm.adoc +++ b/docs/src/reference/asciidoc/sm.adoc @@ -232,8 +232,36 @@ has been completed. State Actions are executed using a normal Spring `TaskScheduler` wrapped within a `Runnable` which may get cancelled via `ScheduledFuture`. What this means is that whatever you're doing in your -action, you need to be able to catch `InterruptedException` which is -raised if task is cancelled. +action, you need to be able to catch `InterruptedException` or generally +periodically check if _Thread_ is interrupted. + +Below shows typical config which uses default _IMMEDIATE_CANCEL_ which +would simply cancel running task immediately when state is complete. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests11.java[tags=snippetA] +---- + +Policy can be set to _TIMEOUT_CANCEL_ together with a global timeout +per machine. This changes state behaviour to wait action completion +before cancel is requested. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests11.java[tags=snippetB] +---- + +If _Event_ directly take machine into a state so that event headers +are available to particular action, it is also possible to use dedicated +event header to instruct a specific timeout which is defined in _millis_. +Reserved header value _StateMachineMessageHeaders.HEADER_DO_ACTION_TIMEOUT_ +is used for this. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests11.java[tags=snippetC] +---- [[statemachine-config-transition-actions-errorhandling]] ==== Transition Action Error Handling diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateMachineMessageHeaders.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateMachineMessageHeaders.java new file mode 100644 index 00000000..4d472981 --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/StateMachineMessageHeaders.java @@ -0,0 +1,33 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.statemachine; + +import org.springframework.messaging.Message; + +/** + * Reserved {@link Message} headers used in {@link StateMachine} events. + * + * @author Janne Valkealahti + * + */ +public final class StateMachineMessageHeaders { + + /** + * Header value defining state do action timeout. Format is a number + * defining timeout as {@code millis}. + */ + public final static String HEADER_DO_ACTION_TIMEOUT = "STATEMACHINE_DO_ACTION_TIMEOUT"; +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/action/StateDoActionPolicy.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/action/StateDoActionPolicy.java new file mode 100644 index 00000000..868b8d1f --- /dev/null +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/action/StateDoActionPolicy.java @@ -0,0 +1,36 @@ +/* + * Copyright 2018 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.action; + +/** + * Enumerations for possible state do action policies. {@code IMMEDIATE_CANCEL} + * is a default setting. + * + * @author Janne Valkealahti + * + */ +public enum StateDoActionPolicy { + + /** + * Policy interrupting action immediately when state is exited. + */ + IMMEDIATE_CANCEL, + + /** + * Policy interrupting action after a timeout before state is exited. + */ + TIMEOUT_CANCEL; +} diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java index f9f4514a..b4b3c7a2 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/AbstractStateMachineFactory.java @@ -232,8 +232,10 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS regions.add(pop.machine); } S parent = (S)peek.getParent(); - RegionState rstate = buildRegionStateInternal(parent, regions, null, stateData != null ? stateData.getEntryActions() : null, - stateData != null ? stateData.getExitActions() : null, new DefaultPseudoState(PseudoStateKind.INITIAL)); + RegionState rstate = buildRegionStateInternal(parent, regions, null, + stateData != null ? stateData.getEntryActions() : null, + stateData != null ? stateData.getExitActions() : null, + new DefaultPseudoState(PseudoStateKind.INITIAL), stateMachineModel); if (stateData != null) { stateMap.put(stateData.getState(), rstate); } else { @@ -580,8 +582,15 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS if (stateData.isInitial()) { pseudoState = new DefaultPseudoState(PseudoStateKind.INITIAL); } - state = new StateMachineState(stateData.getState(), stateMachine, stateData.getDeferred(), - stateData.getEntryActions(), stateData.getExitActions(), pseudoState); + StateMachineState stateMachineState = new StateMachineState(stateData.getState(), + stateMachine, stateData.getDeferred(), stateData.getEntryActions(), stateData.getExitActions(), + pseudoState); + stateMachineState + .setStateDoActionPolicy(stateMachineModel.getConfigurationData().getStateDoActionPolicy()); + stateMachineState.setStateDoActionPolicyTimeout( + stateMachineModel.getConfigurationData().getStateDoActionPolicyTimeout()); + state = stateMachineState; + // TODO: below if/else doesn't feel right if (stateDatas.size() > 1 && stateData.isInitial()) { initialState = state; @@ -901,7 +910,7 @@ public abstract class AbstractStateMachineFactory extends LifecycleObjectS protected abstract RegionState buildRegionStateInternal(S id, Collection> regions, Collection deferred, Collection> entryActions, Collection> exitActions, - PseudoState pseudoState); + PseudoState pseudoState, StateMachineModel stateMachineModel); /** * Simple utility listener waiting machine to get started if diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/ObjectStateMachineFactory.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/ObjectStateMachineFactory.java index c5f58f84..75cfd94b 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/ObjectStateMachineFactory.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/ObjectStateMachineFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2017 the original author or authors. + * Copyright 2015-2018 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. @@ -112,13 +112,19 @@ public class ObjectStateMachineFactory extends AbstractStateMachineFactory if (taskScheduler != null) { objectState.setTaskScheduler(taskScheduler); } + objectState.setStateDoActionPolicy(stateMachineModel.getConfigurationData().getStateDoActionPolicy()); + objectState.setStateDoActionPolicyTimeout(stateMachineModel.getConfigurationData().getStateDoActionPolicyTimeout()); return objectState; } @Override protected RegionState buildRegionStateInternal(S id, Collection> regions, Collection deferred, - Collection> entryActions, Collection> exitActions, PseudoState pseudoState) { - return new RegionState(id, regions, deferred, entryActions, exitActions, pseudoState); + Collection> entryActions, Collection> exitActions, + PseudoState pseudoState, StateMachineModel stateMachineModel) { + RegionState regionState = new RegionState(id, regions, deferred, entryActions, exitActions, pseudoState); + regionState.setStateDoActionPolicy(stateMachineModel.getConfigurationData().getStateDoActionPolicy()); + regionState.setStateDoActionPolicyTimeout(stateMachineModel.getConfigurationData().getStateDoActionPolicyTimeout()); + return regionState; } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationBuilder.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationBuilder.java index 45733425..fb7477b2 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationBuilder.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/builders/StateMachineConfigurationBuilder.java @@ -22,6 +22,7 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.TaskScheduler; import org.springframework.security.access.AccessDecisionManager; +import org.springframework.statemachine.action.StateDoActionPolicy; import org.springframework.statemachine.config.common.annotation.AbstractConfiguredAnnotationBuilder; import org.springframework.statemachine.config.common.annotation.AnnotationBuilder; import org.springframework.statemachine.config.common.annotation.ObjectPostProcessor; @@ -66,6 +67,8 @@ public class StateMachineConfigurationBuilder private TaskScheduler taskScheculer; private boolean autoStart = false; private TransitionConflictPolicy transitionConflictPolicy; + private StateDoActionPolicy stateDoActionPolicy; + private Long stateDoActionPolicyTimeout; private StateMachineEnsemble ensemble; private final List> listeners = new ArrayList>(); private boolean securityEnabled = false; @@ -146,8 +149,9 @@ public class StateMachineConfigurationBuilder } } return new ConfigurationData(beanFactory, taskExecutor, taskScheculer, autoStart, ensemble, listeners, - securityEnabled, transitionSecurityAccessDecisionManager, eventSecurityAccessDecisionManager, eventSecurityRule, - transitionSecurityRule, verifierEnabled, verifier, machineId, stateMachineMonitor, interceptorsCopy, transitionConflictPolicy); + securityEnabled, transitionSecurityAccessDecisionManager, eventSecurityAccessDecisionManager, + eventSecurityRule, transitionSecurityRule, verifierEnabled, verifier, machineId, stateMachineMonitor, + interceptorsCopy, transitionConflictPolicy, stateDoActionPolicy, stateDoActionPolicyTimeout); } /** @@ -303,4 +307,15 @@ public class StateMachineConfigurationBuilder public void setTransitionConflictPolicy(TransitionConflictPolicy transitionConflictPolicy) { this.transitionConflictPolicy = transitionConflictPolicy; } + + /** + * Sets the state do action policy and timeout. + * + * @param stateDoActionPolicy the new state do action policy + * @param stateDoActionPolicyTimeout the new state do action policy timeout + */ + public void setStateDoActionPolicy(StateDoActionPolicy stateDoActionPolicy, Long stateDoActionPolicyTimeout) { + this.stateDoActionPolicy = stateDoActionPolicy; + this.stateDoActionPolicyTimeout = stateDoActionPolicyTimeout; + } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/ConfigurationConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/ConfigurationConfigurer.java index 41e8488a..03ef0d0a 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/ConfigurationConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/ConfigurationConfigurer.java @@ -15,10 +15,13 @@ */ package org.springframework.statemachine.config.configurers; +import java.util.concurrent.TimeUnit; + import org.springframework.beans.factory.BeanFactory; import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.TaskScheduler; import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.action.StateDoActionPolicy; import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerBuilder; import org.springframework.statemachine.listener.StateMachineListener; @@ -88,10 +91,27 @@ public interface ConfigurationConfigurer extends ConfigurationConfigurer listener(StateMachineListener listener); /** - * Speficy a {@link TransitionConflictPolicy}. Default to {@link TransitionConflictPolicy#CHILD}. + * Specify a {@link TransitionConflictPolicy}. Default to {@link TransitionConflictPolicy#CHILD}. * * @param transitionConflictPolicy the transition conflict policy * @return the configuration configurer */ ConfigurationConfigurer transitionConflictPolicy(TransitionConflictPolicy transitionConflictPolicy); + + /** + * Specify a {@link StateDoActionPolicy}. Defaults to {@link StateDoActionPolicy#IMMEDIATE_CANCEL}. + * + * @param stateDoActionPolicy the state do action policy + * @return the configuration configurer + */ + ConfigurationConfigurer stateDoActionPolicy(StateDoActionPolicy stateDoActionPolicy); + + /** + * Specify a timeout used with {@link StateDoActionPolicy}. + * + * @param timeout the timeout + * @param unit the time unit + * @return the configuration configurer + */ + ConfigurationConfigurer stateDoActionPolicyTimeout(long timeout, TimeUnit unit); } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultConfigurationConfigurer.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultConfigurationConfigurer.java index abbd142a..139ee35b 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultConfigurationConfigurer.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/configurers/DefaultConfigurationConfigurer.java @@ -17,10 +17,12 @@ package org.springframework.statemachine.config.configurers; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.BeanFactory; import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.TaskScheduler; +import org.springframework.statemachine.action.StateDoActionPolicy; import org.springframework.statemachine.config.builders.StateMachineConfigurationBuilder; import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerAdapter; @@ -46,6 +48,8 @@ public class DefaultConfigurationConfigurer private TaskScheduler taskScheculer; private boolean autoStart = false; private TransitionConflictPolicy transitionConflightPolicy; + private StateDoActionPolicy stateDoActionPolicy; + private Long stateDoActionPolicyTimeout; private final List> listeners = new ArrayList>(); @Override @@ -57,6 +61,7 @@ public class DefaultConfigurationConfigurer builder.setAutoStart(autoStart); builder.setStateMachineListeners(listeners); builder.setTransitionConflictPolicy(transitionConflightPolicy); + builder.setStateDoActionPolicy(stateDoActionPolicy, stateDoActionPolicyTimeout); } @Override @@ -100,4 +105,16 @@ public class DefaultConfigurationConfigurer this.transitionConflightPolicy = transitionConflightPolicy; return this; } + + @Override + public ConfigurationConfigurer stateDoActionPolicy(StateDoActionPolicy stateDoActionPolicy) { + this.stateDoActionPolicy = stateDoActionPolicy; + return this; + } + + @Override + public ConfigurationConfigurer stateDoActionPolicyTimeout(long timeout, TimeUnit unit) { + this.stateDoActionPolicyTimeout = unit.toMillis(timeout); + return this; + } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/ConfigurationData.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/ConfigurationData.java index 969288d2..62e5cd70 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/ConfigurationData.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/config/model/ConfigurationData.java @@ -24,6 +24,7 @@ import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler; import org.springframework.security.access.AccessDecisionManager; +import org.springframework.statemachine.action.StateDoActionPolicy; import org.springframework.statemachine.config.builders.StateMachineConfigurationBuilder; import org.springframework.statemachine.config.model.verifier.DefaultStateMachineModelVerifier; import org.springframework.statemachine.config.model.verifier.StateMachineModelVerifier; @@ -50,6 +51,8 @@ public class ConfigurationData { private final TaskScheduler taskScheduler; private final boolean autoStart; private final TransitionConflictPolicy transitionConflictPolicy; + private final StateDoActionPolicy stateDoActionPolicy; + private final Long stateDoActionPolicyTimeout; private final StateMachineEnsemble ensemble; private final List> listeners; private final boolean securityEnabled; @@ -99,7 +102,7 @@ public class ConfigurationData { List> interceptors) { this(beanFactory, taskExecutor, taskScheduler, autoStart, ensemble, listeners, securityEnabled, transitionSecurityAccessDecisionManager, eventSecurityAccessDecisionManager, eventSecurityRule, transitionSecurityRule, - verifierEnabled, verifier, machineId, stateMachineMonitor, interceptors, null); + verifierEnabled, verifier, machineId, stateMachineMonitor, interceptors, null, null, null); } /** @@ -122,6 +125,8 @@ public class ConfigurationData { * @param stateMachineMonitor the state machine monitor * @param interceptors the state machine interceptors. * @param transitionConflightPolicy the transition conflict policy + * @param stateDoActionPolicy the state do action policy + * @param stateDoActionPolicyTimeout the state do action policy timeout */ public ConfigurationData(BeanFactory beanFactory, TaskExecutor taskExecutor, TaskScheduler taskScheduler, boolean autoStart, StateMachineEnsemble ensemble, @@ -129,7 +134,8 @@ public class ConfigurationData { AccessDecisionManager transitionSecurityAccessDecisionManager, AccessDecisionManager eventSecurityAccessDecisionManager, SecurityRule eventSecurityRule, SecurityRule transitionSecurityRule, boolean verifierEnabled, StateMachineModelVerifier verifier, String machineId, StateMachineMonitor stateMachineMonitor, - List> interceptors, TransitionConflictPolicy transitionConflightPolicy) { + List> interceptors, TransitionConflictPolicy transitionConflightPolicy, + StateDoActionPolicy stateDoActionPolicy, Long stateDoActionPolicyTimeout) { this.beanFactory = beanFactory; this.taskExecutor = taskExecutor; this.taskScheduler = taskScheduler; @@ -147,6 +153,8 @@ public class ConfigurationData { this.stateMachineMonitor = stateMachineMonitor; this.interceptors = interceptors; this.transitionConflictPolicy = transitionConflightPolicy; + this.stateDoActionPolicy = stateDoActionPolicy; + this.stateDoActionPolicyTimeout = stateDoActionPolicyTimeout; } public String getMachineId() { @@ -296,4 +304,22 @@ public class ConfigurationData { public TransitionConflictPolicy getTransitionConflictPolicy() { return transitionConflictPolicy; } + + /** + * Gets the state do action policy. + * + * @return the state do action policy + */ + public StateDoActionPolicy getStateDoActionPolicy() { + return stateDoActionPolicy; + } + + /** + * Gets the state do action policy timeout. + * + * @return the state do action policy timeout + */ + public Long getStateDoActionPolicyTimeout() { + return stateDoActionPolicyTimeout; + } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java index 98ab9797..6c7d7fbf 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java @@ -22,6 +22,7 @@ import java.util.Date; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.logging.Log; @@ -34,10 +35,12 @@ import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.action.ActionListener; import org.springframework.statemachine.action.CompositeActionListener; +import org.springframework.statemachine.action.StateDoActionPolicy; import org.springframework.statemachine.listener.StateMachineListener; import org.springframework.statemachine.listener.StateMachineListenerAdapter; import org.springframework.statemachine.region.Region; import org.springframework.statemachine.support.LifecycleObjectSupport; +import org.springframework.statemachine.support.StateMachineUtils; import org.springframework.statemachine.trigger.Trigger; /** @@ -61,9 +64,11 @@ public abstract class AbstractState extends LifecycleObjectSupport impleme private final StateMachine submachine; private List> triggers = new ArrayList>(); private final CompositeStateListener stateListener = new CompositeStateListener(); - private final List> cancellableActions = new ArrayList<>(); + private final List scheduledActions = new ArrayList<>(); private CompositeActionListener actionListener; private final List> completionListeners = new CopyOnWriteArrayList<>(); + private StateDoActionPolicy stateDoActionPolicy; + private Long stateDoActionPolicyTimeout; /** * Instantiates a new abstract state. @@ -399,6 +404,14 @@ public abstract class AbstractState extends LifecycleObjectSupport impleme return triggers; } + public void setStateDoActionPolicy(StateDoActionPolicy stateDoActionPolicy) { + this.stateDoActionPolicy = stateDoActionPolicy; + } + + public void setStateDoActionPolicyTimeout(Long stateDoActionPolicyTimeout) { + this.stateDoActionPolicyTimeout = stateDoActionPolicyTimeout; + } + /** * Arm triggers. */ @@ -421,10 +434,30 @@ public abstract class AbstractState extends LifecycleObjectSupport impleme * Cancel existing state actions and clear list. */ protected void cancelStateActions() { - for (ScheduledFuture future : cancellableActions) { - future.cancel(true); + if (log.isDebugEnabled()) { + log.debug("Handling finish of state actions, scheduledActions size is " + scheduledActions.size()); } - cancellableActions.clear(); + for (ScheduledAction task : scheduledActions) { + if (task.timeout != null) { + if (log.isDebugEnabled()) { + log.debug("Timeouting scheduled state do action " + task); + } + try { + task.future.get(task.timeout, TimeUnit.MILLISECONDS); + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug("Cancelling scheduled state do action after timeout " + task); + } + task.future.cancel(true); + } + } else { + if (log.isDebugEnabled()) { + log.debug("Cancelling scheduled state do action immediately " + task); + } + task.future.cancel(true); + } + } + scheduledActions.clear(); } /** @@ -440,8 +473,11 @@ public abstract class AbstractState extends LifecycleObjectSupport impleme } for (Action action : stateActions) { ScheduledFuture future = scheduleAction(action, context, completionCount); + if (log.isDebugEnabled()) { + log.debug("Scheduling state do action " + action + " with future " + future); + } if (future != null) { - cancellableActions.add(future); + scheduledActions.add(new ScheduledAction(future, resolveDoActionTimeout(context))); } } if (isSimple() && stateActions.size() == 0) { @@ -475,7 +511,8 @@ public abstract class AbstractState extends LifecycleObjectSupport impleme * @param completionCount the completion count tracker * @return the scheduled future */ - protected ScheduledFuture scheduleAction(final Action action, final StateContext context, final AtomicInteger completionCount) { + protected ScheduledFuture scheduleAction(final Action action, final StateContext context, + final AtomicInteger completionCount) { TaskScheduler taskScheduler = getTaskScheduler(); if (taskScheduler == null) { log.error("Unable to schedule action as taskSchedule is not set, action=[" + action + "]"); @@ -498,6 +535,31 @@ public abstract class AbstractState extends LifecycleObjectSupport impleme stateListener.onComplete(context); } + private Long resolveDoActionTimeout(StateContext context) { + Long timeout = null; + if (stateDoActionPolicy == StateDoActionPolicy.TIMEOUT_CANCEL) { + timeout = StateMachineUtils.getMessageHeaderDoActionTimeout(context); + if (timeout == null) { + timeout = stateDoActionPolicyTimeout; + } + } + return timeout; + } + + private static class ScheduledAction { + ScheduledFuture future; + Long timeout; + + public ScheduledAction(ScheduledFuture future, Long timeout) { + this.future = future; + this.timeout = timeout; + } + @Override + public String toString() { + return "ScheduledTask [future=" + future + ", timeout=" + timeout + "]"; + } + } + @Override public String toString() { return "AbstractState [id=" + id + ", pseudoState=" + pseudoState + ", deferred=" + deferred + ", entryActions=" diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineUtils.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineUtils.java index 009f8800..6c4f7db0 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineUtils.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/StateMachineUtils.java @@ -18,6 +18,8 @@ package org.springframework.statemachine.support; import java.util.ArrayList; import java.util.Collection; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.StateMachineMessageHeaders; import org.springframework.statemachine.state.PseudoState; import org.springframework.statemachine.state.PseudoStateKind; import org.springframework.statemachine.state.State; @@ -170,4 +172,23 @@ public abstract class StateMachineUtils { return false; } + /** + * Gets a particular message header from a {@link StateContext} and parses + * it to {@code Long}. Any error parsing header results {@code NULL}. + * + * @param the type of state + * @param the type of event + * @param context the state context + * @return the header as {@code Long} or {@code NULL} + */ + public static Long getMessageHeaderDoActionTimeout(StateContext context) { + try { + Number number = context.getMessageHeaders().get(StateMachineMessageHeaders.HEADER_DO_ACTION_TIMEOUT, Number.class); + if (number != null) { + return number.longValue(); + } + } catch (Exception e) { + } + return null; + } } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/action/StateDoActivityActionTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/action/StateDoActivityActionTests.java index 2f32b0c3..763f2ad4 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/action/StateDoActivityActionTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/action/StateDoActivityActionTests.java @@ -26,11 +26,14 @@ import org.junit.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.support.MessageBuilder; import org.springframework.statemachine.AbstractStateMachineTests; +import org.springframework.statemachine.StateMachineMessageHeaders; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.StateMachineSystemConstants; import org.springframework.statemachine.config.EnableStateMachine; import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; @@ -247,6 +250,201 @@ public class StateDoActivityActionTests extends AbstractStateMachineTests { } } + @Test + public void testStateDoActionNotCancelledWithEventTimeout() throws Exception { + context.register(Config4.class); + context.refresh(); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestSleepAction testActionS2 = context.getBean("testActionS2", TestSleepAction.class); + + assertThat(machine, notNullValue()); + machine.start(); + + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1) + .setHeader(StateMachineMessageHeaders.HEADER_DO_ACTION_TIMEOUT, 4000).build()); + assertThat(testActionS2.onExecuteStartLatch.await(2, TimeUnit.SECONDS), is(true)); + machine.sendEvent(TestEvents.E2); + assertThat(testActionS2.onExecuteLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(testActionS2.interruptedLatch.await(2, TimeUnit.SECONDS), is(false)); + } + + @Test + public void testStateDoActionCancelledWithEventTimeout() throws Exception { + context.register(Config4.class); + context.refresh(); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestSleepAction testActionS2 = context.getBean("testActionS2", TestSleepAction.class); + + assertThat(machine, notNullValue()); + machine.start(); + + machine.sendEvent(MessageBuilder.withPayload(TestEvents.E1) + .setHeader(StateMachineMessageHeaders.HEADER_DO_ACTION_TIMEOUT, 100).build()); + assertThat(testActionS2.onExecuteStartLatch.await(2, TimeUnit.SECONDS), is(true)); + machine.sendEvent(TestEvents.E2); + assertThat(testActionS2.onExecuteLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(testActionS2.interruptedLatch.await(2, TimeUnit.SECONDS), is(true)); + } + + @Test + public void testStateDoActionCancelledWithConfigSetting() throws Exception { + context.register(Config5.class); + context.refresh(); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestSleepAction testActionS2 = context.getBean("testActionS2", TestSleepAction.class); + + assertThat(machine, notNullValue()); + machine.start(); + + machine.sendEvent(TestEvents.E1); + assertThat(testActionS2.onExecuteStartLatch.await(2, TimeUnit.SECONDS), is(true)); + machine.sendEvent(TestEvents.E2); + assertThat(testActionS2.onExecuteLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(testActionS2.interruptedLatch.await(2, TimeUnit.SECONDS), is(true)); + } + + @Test + public void testStateDoActionNotCancelledWithConfigTimeout() throws Exception { + context.register(Config6.class); + context.refresh(); + StateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class); + TestSleepAction testActionS2 = context.getBean("testActionS2", TestSleepAction.class); + + assertThat(machine, notNullValue()); + machine.start(); + + machine.sendEvent(TestEvents.E1); + assertThat(testActionS2.onExecuteStartLatch.await(2, TimeUnit.SECONDS), is(true)); + machine.sendEvent(TestEvents.E2); + assertThat(testActionS2.onExecuteLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(testActionS2.interruptedLatch.await(2, TimeUnit.SECONDS), is(false)); + } + + @Configuration + @EnableStateMachine + static class Config4 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .stateDoActionPolicy(StateDoActionPolicy.TIMEOUT_CANCEL); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S2, testActionS2()) + .state(TestStates.S3); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S2) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S2) + .target(TestStates.S3) + .event(TestEvents.E2); + } + + @Bean + public TestSleepAction testActionS2() { + return new TestSleepAction(2000); + } + } + + @Configuration + @EnableStateMachine + static class Config5 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .stateDoActionPolicy(StateDoActionPolicy.IMMEDIATE_CANCEL); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S2, testActionS2()) + .state(TestStates.S3); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S2) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S2) + .target(TestStates.S3) + .event(TestEvents.E2); + } + + @Bean + public TestSleepAction testActionS2() { + return new TestSleepAction(2000); + } + } + + @Configuration + @EnableStateMachine + static class Config6 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .stateDoActionPolicy(StateDoActionPolicy.TIMEOUT_CANCEL) + .stateDoActionPolicyTimeout(10, TimeUnit.SECONDS); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S2, testActionS2()) + .state(TestStates.S3); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S2) + .event(TestEvents.E1) + .and() + .withExternal() + .source(TestStates.S2) + .target(TestStates.S3) + .event(TestEvents.E2); + } + + @Bean + public TestSleepAction testActionS2() { + return new TestSleepAction(2000); + } + } + @Override protected AnnotationConfigApplicationContext buildContext() { return new AnnotationConfigApplicationContext(); diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests11.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests11.java new file mode 100644 index 00000000..77b1c933 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests11.java @@ -0,0 +1,126 @@ +/* + * Copyright 2018 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.docs; + +import java.util.concurrent.TimeUnit; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.statemachine.AbstractStateMachineTests; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.StateMachineMessageHeaders; +import org.springframework.statemachine.action.StateDoActionPolicy; +import org.springframework.statemachine.config.EnableStateMachine; +import org.springframework.statemachine.config.StateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; + +public class DocsConfigurationSampleTests11 extends AbstractStateMachineTests { + +// tag::snippetA[] + @Configuration + @EnableStateMachine + static class Config1 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .stateDoActionPolicy(StateDoActionPolicy.IMMEDIATE_CANCEL); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S1") + .state("S2", context -> {}) + .state("S3"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S1") + .target("S2") + .event("E1") + .and() + .withExternal() + .source("S2") + .target("S3") + .event("E2"); + } + } +// end::snippetA[] + + @Configuration + @EnableStateMachine + static class Config2 extends StateMachineConfigurerAdapter { + +// tag::snippetB[] + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .stateDoActionPolicy(StateDoActionPolicy.TIMEOUT_CANCEL) + .stateDoActionPolicyTimeout(10, TimeUnit.SECONDS); + } +// end::snippetB[] + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S1") + .state("S2", context -> {}) + .state("S3"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S1") + .target("S2") + .event("E1") + .and() + .withExternal() + .source("S2") + .target("S3") + .event("E2"); + } + } + + static class Example1 { + +// tag::snippetC[] + @Autowired + StateMachine stateMachine; + + void sendEventUsingTimeout() { + stateMachine.sendEvent(MessageBuilder + .withPayload("E1") + .setHeader(StateMachineMessageHeaders.HEADER_DO_ACTION_TIMEOUT, 5000) + .build()); + + } +// end::snippetC[] + } + +}