Timer interval reset for states

- Add new features around Trigger to arm/disarm
  it so that state entry/exit can stop firing
  TimerTrigger. Default functionality for timer(long)
  still stays same, however timerOnce(long) will
  kick this new functionality where trigger will fire
  once after state has been entered.
- Fixes #165
This commit is contained in:
Janne Valkealahti
2016-03-05 11:28:34 +00:00
parent 8b560f5bba
commit 28a23cbf15
17 changed files with 401 additions and 26 deletions

View File

@@ -51,6 +51,7 @@ import org.springframework.statemachine.ensemble.DistributedStateMachine;
import org.springframework.statemachine.listener.StateMachineListener;
import org.springframework.statemachine.region.Region;
import org.springframework.statemachine.security.StateMachineSecurityInterceptor;
import org.springframework.statemachine.state.AbstractState;
import org.springframework.statemachine.state.ChoicePseudoState;
import org.springframework.statemachine.state.ChoicePseudoState.ChoiceStateData;
import org.springframework.statemachine.state.DefaultPseudoState;
@@ -547,12 +548,13 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
S target = transitionData.getTarget();
E event = transitionData.getEvent();
Long period = transitionData.getPeriod();
Integer count = transitionData.getCount();
Trigger<S, E> trigger = null;
if (event != null) {
trigger = new EventTrigger<S, E>(event);
} else if (period != null) {
TimerTrigger<S, E> t = new TimerTrigger<S, E>(period);
TimerTrigger<S, E> t = new TimerTrigger<S, E>(period, count != null ? count : 0);
if (beanFactory != null) {
t.setBeanFactory(beanFactory);
}
@@ -563,6 +565,7 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
t.setTaskScheduler(taskScheduler);
}
trigger = t;
((AbstractState<S, E>)stateMap.get(source)).getTriggers().add(trigger);
}
if (transitionData.getKind() == TransitionKind.EXTERNAL) {

View File

@@ -111,7 +111,7 @@ public class StateMachineTransitionBuilder<S, E>
return apply(new DefaultJoinTransitionConfigurer<S, E>());
}
public void add(S source, S target, S state, E event, Long period, Collection<Action<S, E>> actions,
public void add(S source, S target, S state, E event, Long period, Integer count, Collection<Action<S, E>> actions,
Guard<S, E> guard, TransitionKind kind, SecurityRule securityRule) {
// if rule not given, get it from global
if (securityRule == null) {
@@ -119,7 +119,7 @@ public class StateMachineTransitionBuilder<S, E>
StateMachineConfigurationConfig<S, E> config = getSharedObject(StateMachineConfigurationConfig.class);
securityRule = config.getTransitionSecurityRule();
}
transitionData.add(new TransitionData<S, E>(source, target, state, event, period, actions, guard, kind, securityRule));
transitionData.add(new TransitionData<S, E>(source, target, state, event, period, count, actions, guard, kind, securityRule));
}
public void add(S source, List<ChoiceData<S, E>> choices) {

View File

@@ -43,6 +43,7 @@ public abstract class AbstractTransitionConfigurer<S, E> extends
private S state;
private E event;
private Long period;
private Integer count;
private final Collection<Action<S, E>> actions = new ArrayList<Action<S, E>>();
private Guard<S, E> guard;
private SecurityRule securityRule;
@@ -67,6 +68,10 @@ public abstract class AbstractTransitionConfigurer<S, E> extends
return period;
}
public Integer getCount() {
return count;
}
protected Collection<Action<S, E>> getActions() {
return actions;
}
@@ -99,6 +104,10 @@ public abstract class AbstractTransitionConfigurer<S, E> extends
this.period = period;
}
public void setCount(Integer count) {
this.count = count;
}
protected void addAction(Action<S, E> action) {
this.actions.add(action);
}

View File

@@ -38,7 +38,7 @@ public class DefaultExternalTransitionConfigurer<S, E> extends AbstractTransitio
@Override
public void configure(StateMachineTransitionBuilder<S, E> builder) throws Exception {
builder.add(getSource(), getTarget(), getState(), getEvent(), getPeriod(), getActions(), getGuard(), TransitionKind.EXTERNAL,
builder.add(getSource(), getTarget(), getState(), getEvent(), getPeriod(), getCount(), getActions(), getGuard(), TransitionKind.EXTERNAL,
getSecurityRule());
}
@@ -72,6 +72,13 @@ public class DefaultExternalTransitionConfigurer<S, E> extends AbstractTransitio
return this;
}
@Override
public ExternalTransitionConfigurer<S, E> timerOnce(long period) {
setPeriod(period);
setCount(1);
return this;
}
@Override
public ExternalTransitionConfigurer<S, E> action(Action<S, E> action) {
addAction(action);

View File

@@ -38,7 +38,7 @@ public class DefaultInternalTransitionConfigurer<S, E> extends AbstractTransitio
@Override
public void configure(StateMachineTransitionBuilder<S, E> builder) throws Exception {
builder.add(getSource(), getTarget(), getState(), getEvent(), getPeriod(), getActions(), getGuard(), TransitionKind.INTERNAL,
builder.add(getSource(), getTarget(), getState(), getEvent(), getPeriod(), getCount(), getActions(), getGuard(), TransitionKind.INTERNAL,
getSecurityRule());
}
@@ -66,6 +66,13 @@ public class DefaultInternalTransitionConfigurer<S, E> extends AbstractTransitio
return this;
}
@Override
public InternalTransitionConfigurer<S, E> timerOnce(long period) {
setPeriod(period);
setCount(1);
return this;
}
@Override
public InternalTransitionConfigurer<S, E> action(Action<S, E> action) {
addAction(action);

View File

@@ -37,7 +37,7 @@ public class DefaultLocalTransitionConfigurer<S, E> extends AbstractTransitionCo
@Override
public void configure(StateMachineTransitionBuilder<S, E> builder) throws Exception {
builder.add(getSource(), getTarget(), getState(), getEvent(), getPeriod(), getActions(), getGuard(), TransitionKind.LOCAL,
builder.add(getSource(), getTarget(), getState(), getEvent(), getPeriod(), getCount(), getActions(), getGuard(), TransitionKind.LOCAL,
getSecurityRule());
}
@@ -71,6 +71,13 @@ public class DefaultLocalTransitionConfigurer<S, E> extends AbstractTransitionCo
return this;
}
@Override
public LocalTransitionConfigurer<S, E> timerOnce(long period) {
setPeriod(period);
setCount(1);
return this;
}
@Override
public LocalTransitionConfigurer<S, E> action(Action<S, E> action) {
addAction(action);

View File

@@ -67,6 +67,14 @@ public interface TransitionConfigurer<T, S, E> extends
*/
T timer(long period);
/**
* Specify that this transition is triggered once by a time after a delay.
*
* @param period timer period in millis
* @return configurer for chaining
*/
T timerOnce(long period);
/**
* Specify {@link Action} for this {@link Transition}.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2016 the original author or authors.
* Copyright 2016 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.
@@ -34,6 +34,7 @@ public class TransitionData<S, E> {
private final S state;
private final E event;
private final Long period;
private final Integer count;
private final Collection<Action<S, E>> actions;
private final Guard<S, E> guard;
private final TransitionKind kind;
@@ -47,18 +48,20 @@ public class TransitionData<S, E> {
* @param state the state
* @param event the event
* @param period the period
* @param count the count
* @param actions the actions
* @param guard the guard
* @param kind the kind
* @param securityRule the security rule
*/
public TransitionData(S source, S target, S state, E event, Long period, Collection<Action<S, E>> actions,
public TransitionData(S source, S target, S state, E event, Long period, Integer count, Collection<Action<S, E>> actions,
Guard<S, E> guard, TransitionKind kind, SecurityRule securityRule) {
this.source = source;
this.target = target;
this.state = state;
this.event = event;
this.period = period;
this.count = count;
this.actions = actions;
this.guard = guard;
this.kind = kind;
@@ -110,6 +113,15 @@ public class TransitionData<S, E> {
return period;
}
/**
* Gets the count.
*
* @return the count
*/
public Integer getCount() {
return count;
}
/**
* Gets the actions.
*

View File

@@ -17,12 +17,14 @@ package org.springframework.statemachine.state;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.messaging.Message;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.region.Region;
import org.springframework.statemachine.trigger.Trigger;
/**
* Base implementation of a {@link State}.
@@ -41,6 +43,7 @@ public abstract class AbstractState<S, E> implements State<S, E> {
private final Collection<? extends Action<S, E>> exitActions;
private final Collection<Region<S, E>> regions = new ArrayList<Region<S, E>>();
private final StateMachine<S, E> submachine;
private List<Trigger<S, E>> triggers = new ArrayList<Trigger<S, E>>();
/**
* Instantiates a new abstract state.
@@ -158,10 +161,18 @@ public abstract class AbstractState<S, E> implements State<S, E> {
}
@Override
public abstract void exit(StateContext<S, E> context);
public void exit(StateContext<S, E> context) {
for (Trigger<S, E> trigger : triggers) {
trigger.disarm();
}
}
@Override
public abstract void entry(StateContext<S, E> context);
public void entry(StateContext<S, E> context) {
for (Trigger<S, E> trigger : triggers) {
trigger.arm();
}
}
@Override
public S getId() {
@@ -232,6 +243,14 @@ public abstract class AbstractState<S, E> implements State<S, E> {
return regions;
}
public void setTriggers(List<Trigger<S, E>> triggers) {
this.triggers = triggers;
}
public List<Trigger<S, E>> getTriggers() {
return triggers;
}
@Override
public String toString() {
return "AbstractState [id=" + id + ", pseudoState=" + pseudoState + ", deferred=" + deferred

View File

@@ -0,0 +1,138 @@
/*
* Copyright 2016 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.state;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.support.PeriodicTrigger;
import org.springframework.util.Assert;
/**
* Enhanced implementation following same logic from {@link PeriodicTrigger}
* except also adding a counter how many times a trigger can fire. If given
* count is either zero on a negative value, counter functionality is disabled.
*
* @author Janne Valkealahti
* @see PeriodicTrigger
*/
public class CountTrigger implements Trigger {
private final int count;
private final long period;
private final TimeUnit timeUnit;
private volatile long initialDelay = 0;
private volatile boolean fixedRate = false;
private volatile int counter = 0;
/**
* Create a trigger with the given period in milliseconds and firing
* exactly one time.
*
* @param period the period
*/
public CountTrigger(long period) {
this(1, period, null);
}
/**
* Create a trigger with the given count, period and time unit. The time unit will
* apply not only to the period but also to any 'initialDelay' value, if
* configured on this Trigger later via {@link #setInitialDelay(long)}.
*
* @param count the count
* @param period the period
* @param timeUnit the time unit
*/
public CountTrigger(int count, long period, TimeUnit timeUnit) {
Assert.isTrue(period >= 0, "period must not be negative");
Assert.isTrue(count >= 0, "count must not be negative");
this.timeUnit = (timeUnit != null ? timeUnit : TimeUnit.MILLISECONDS);
this.period = this.timeUnit.toMillis(period);
this.count = count;
}
/**
* Specify the delay for the initial execution. It will be evaluated in
* terms of this trigger's {@link TimeUnit}. If no time unit was explicitly
* provided upon instantiation, the default is milliseconds.
*
* @param initialDelay the new initial delay
*/
public void setInitialDelay(long initialDelay) {
this.initialDelay = this.timeUnit.toMillis(initialDelay);
}
/**
* Specify whether the periodic interval should be measured between the
* scheduled start times rather than between actual completion times.
* The latter, "fixed delay" behavior, is the default.
*
* @param fixedRate the new fixed rate
*/
public void setFixedRate(boolean fixedRate) {
this.fixedRate = fixedRate;
}
@Override
public Date nextExecutionTime(TriggerContext triggerContext) {
if (count > 0) {
if (++counter > count) {
return null;
}
}
if (triggerContext.lastScheduledExecutionTime() == null) {
return new Date(System.currentTimeMillis() + this.initialDelay);
}
else if (this.fixedRate) {
return new Date(triggerContext.lastScheduledExecutionTime().getTime() + this.period);
}
return new Date(triggerContext.lastCompletionTime().getTime() + this.period);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + count;
result = prime * result + (fixedRate ? 1231 : 1237);
result = prime * result + (int) (initialDelay ^ (initialDelay >>> 32));
result = prime * result + (int) (period ^ (period >>> 32));
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
CountTrigger other = (CountTrigger) obj;
if (count != other.count)
return false;
if (fixedRate != other.fixedRate)
return false;
if (initialDelay != other.initialDelay)
return false;
if (period != other.period)
return false;
return true;
}
}

View File

@@ -123,6 +123,7 @@ public class ObjectState<S, E> extends AbstractSimpleState<S, E> {
@Override
public void exit(StateContext<S, E> context) {
super.exit(context);
Collection<? extends Action<S, E>> actions = getExitActions();
if (actions != null) {
for (Action<S, E> action : actions) {
@@ -137,6 +138,7 @@ public class ObjectState<S, E> extends AbstractSimpleState<S, E> {
@Override
public void entry(StateContext<S, E> context) {
super.entry(context);
Collection<? extends Action<S, E>> actions = getEntryActions();
if (actions != null) {
for (Action<S, E> action : actions) {

View File

@@ -416,7 +416,9 @@ public class DefaultStateMachineExecutor<S, E> extends LifecycleObjectSupport im
((TimerTrigger<?, ?>) trigger).addTriggerListener(new TriggerListener() {
@Override
public void triggered() {
log.debug("TimedTrigger triggered " + trigger);
if (log.isDebugEnabled()) {
log.debug("TimedTrigger triggered " + trigger);
}
triggerQueue.add(new TriggerQueueItem(trigger, null));
scheduleEventQueueProcessing();
}

View File

@@ -40,4 +40,13 @@ public class EventTrigger<S, E> implements Trigger<S, E> {
return event;
}
@Override
public void arm() {
// no-opt
}
@Override
public void disarm() {
// no-opt
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2016 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.
@@ -16,19 +16,45 @@
package org.springframework.statemachine.trigger;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.springframework.statemachine.state.CountTrigger;
import org.springframework.statemachine.support.LifecycleObjectSupport;
/**
* Implementation of a {@link Trigger} capable of firing on a
* static periods.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public class TimerTrigger<S, E> extends LifecycleObjectSupport implements Trigger<S, E> {
private final CompositeTriggerListener triggerListener = new CompositeTriggerListener();
private final long period;
private final int count;
private volatile ScheduledFuture<?> scheduled;
/**
* Instantiates a new timer trigger.
*
* @param period the period in milliseconds
*/
public TimerTrigger(long period) {
this(period, 0);
}
/**
* Instantiates a new timer trigger.
*
* @param period the period
* @param count the count
*/
public TimerTrigger(long period, int count) {
this.period = period;
this.count = count;
}
@Override
@@ -48,25 +74,50 @@ public class TimerTrigger<S, E> extends LifecycleObjectSupport implements Trigge
@Override
protected void doStart() {
scheduled = getTaskScheduler().scheduleAtFixedRate(new Runnable() {
if (count > 0) {
return;
}
schedule();
}
@Override
protected void doStop() {
cancel();
}
@Override
public void arm() {
if (scheduled != null) {
return;
}
schedule();
}
@Override
public void disarm() {
if (count > 0) {
cancel();
}
}
private void schedule() {
scheduled = getTaskScheduler().schedule(new Runnable() {
@Override
public void run() {
notifyTriggered();
}
}, period);
}
@Override
protected void doStop() {
if (scheduled != null) {
scheduled.cancel(true);
}
scheduled = null;
}, new CountTrigger(count, period, TimeUnit.MILLISECONDS));
}
private void notifyTriggered() {
triggerListener.triggered();
}
private void cancel() {
if (scheduled != null) {
scheduled.cancel(true);
}
scheduled = null;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2016 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.
@@ -51,4 +51,15 @@ public interface Trigger<S,E> {
*/
E getEvent();
/**
* Arm a trigger. After trigger has been armed a {@link TriggerListener}
* may receive events.
*/
void arm();
/**
* Disarm a trigger. After trigger has been disarmed a {@link TriggerListener}
* will not receive events.
*/
void disarm();
}

View File

@@ -18,8 +18,11 @@ package org.springframework.statemachine.trigger;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -30,6 +33,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.AbstractStateMachineTests;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.TestUtils;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
@@ -119,6 +123,55 @@ public class TimerTriggerTests extends AbstractStateMachineTests {
}
}
private class TestTriggerListener implements TriggerListener {
CountDownLatch latch = new CountDownLatch(1);
@Override
public void triggered() {
latch.countDown();
}
}
@SuppressWarnings("unchecked")
@Test
public void testTimerDelayFireOnlyOnState() throws Exception {
context.register(BaseConfig.class, Config4.class);
context.refresh();
StateMachine<TestStates, TestEvents> machine = context.getBean(StateMachine.class);
TestTimerAction action = context.getBean("testTimerAction", TestTimerAction.class);
TestListener listener = new TestListener();
machine.addStateListener(listener);
TimerTrigger<?, ?> trigger = null;
Map<Trigger<?, ?>, Transition<?, ?>> triggerToTransitionMap = TestUtils.readField("triggerToTransitionMap", machine);
for (Entry<Trigger<?, ?>, Transition<?, ?>> entry : triggerToTransitionMap.entrySet()) {
if (entry.getKey() instanceof TimerTrigger) {
trigger = (TimerTrigger<?, ?>) entry.getKey();
continue;
}
}
assertThat(trigger, notNullValue());
TestTriggerListener tlistener = new TestTriggerListener();
trigger.addTriggerListener(tlistener);
machine.start();
assertThat(listener.stateMachineStartedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S1));
assertThat(tlistener.latch.await(2, TimeUnit.SECONDS), is(false));
listener.reset(1);
machine.sendEvent(TestEvents.E1);
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedCount, is(1));
assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S2));
assertThat(action.latch.await(2, TimeUnit.SECONDS), is(true));
action.reset(1);
assertThat(action.latch.await(2, TimeUnit.SECONDS), is(false));
}
static class Config1 {
@Bean
@@ -211,6 +264,39 @@ public class TimerTriggerTests extends AbstractStateMachineTests {
}
@Configuration
@EnableStateMachine
static class Config4 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
states
.withStates()
.initial(TestStates.S1)
.state(TestStates.S2);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
transitions
.withExternal()
.source(TestStates.S1)
.target(TestStates.S2)
.event(TestEvents.E1)
.and()
.withInternal()
.source(TestStates.S2)
.action(testTimerAction())
.timerOnce(1000);
}
@Bean
public TestTimerAction testTimerAction() {
return new TestTimerAction();
}
}
private static class TestTimerAction implements Action<TestStates, TestEvents> {
int count = 0;
@@ -222,6 +308,10 @@ public class TimerTriggerTests extends AbstractStateMachineTests {
latch.countDown();
}
void reset(int a) {
latch = new CountDownLatch(a);
count = 0;
}
}
private static class TestTimerAction2 implements Action<String, String> {

View File

@@ -182,7 +182,7 @@ public class CdPlayerTests {
assertThat(listener.transitionCount, is(1));
listener.reset(0, 0, 0, 2);
assertThat(listener.transitionLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.transitionLatch.await(2100, TimeUnit.MILLISECONDS), is(true));
assertThat(listener.transitionCount, is(2));
assertLcdStatusNotContains("00:02");
}