NPE on sendEvent
- Adding null check for current state - Remove synchronization from scheduleEventQueueProcessing method in executor. Looks like this sync is not really needed and indeed may cause jvm level deadlocks if threads are used for execution. - Change how some internals in AbstractStateMachine are synchronized to limit changes for deadlock. - Relates to #307 - Backport of #359 #360
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.statemachine.buildtests;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.statemachine.StateMachine;
|
||||
import org.springframework.statemachine.config.StateMachineBuilder;
|
||||
import org.springframework.statemachine.test.StateMachineTestPlan;
|
||||
import org.springframework.statemachine.test.StateMachineTestPlanBuilder;
|
||||
|
||||
public class TimerSmokeTests {
|
||||
|
||||
private static ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
|
||||
{
|
||||
taskExecutor.initialize();
|
||||
}
|
||||
|
||||
private StateMachine<String, String> buildMachine() throws Exception {
|
||||
|
||||
StateMachineBuilder.Builder<String, String> builder = StateMachineBuilder.builder();
|
||||
|
||||
builder.configureConfiguration()
|
||||
.withConfiguration()
|
||||
.taskExecutor(taskExecutor);
|
||||
|
||||
builder.configureStates()
|
||||
.withStates()
|
||||
.initial("initial")
|
||||
.end("end");
|
||||
|
||||
builder.configureTransitions()
|
||||
.withExternal()
|
||||
.source("initial")
|
||||
.target("end")
|
||||
.timerOnce(30)
|
||||
.and()
|
||||
.withLocal()
|
||||
.source("initial")
|
||||
.event("repeate");
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private StateMachine<String, String> buildMachine2() throws Exception {
|
||||
|
||||
StateMachineBuilder.Builder<String, String> builder = StateMachineBuilder.builder();
|
||||
|
||||
builder.configureConfiguration()
|
||||
.withConfiguration()
|
||||
.taskExecutor(taskExecutor);
|
||||
|
||||
builder.configureStates()
|
||||
.withStates()
|
||||
.initial("initial").end("end").and()
|
||||
.withStates().parent("initial").initial("inner");
|
||||
|
||||
builder.configureTransitions()
|
||||
.withExternal()
|
||||
.source("initial")
|
||||
.target("end")
|
||||
.timerOnce(30)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source("inner")
|
||||
.target("end")
|
||||
.timerOnce(15)
|
||||
.and()
|
||||
.withLocal()
|
||||
.source("inner")
|
||||
.event("repeate");
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNPE() throws Exception {
|
||||
StateMachine<String, String> stateMachine;
|
||||
for (int i = 0; i < 20; i++) {
|
||||
stateMachine = buildMachine();
|
||||
stateMachine.start();
|
||||
while (!stateMachine.isComplete()) {
|
||||
stateMachine.sendEvent("repeate");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNPE2() throws Exception {
|
||||
|
||||
StateMachine<String, String> stateMachine;
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
stateMachine = buildMachine2();
|
||||
stateMachine.start();
|
||||
while(!stateMachine.isComplete()) {
|
||||
stateMachine.sendEvent("repeate");
|
||||
}
|
||||
stateMachine.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeadlock() throws Exception {
|
||||
StateMachineTestPlan<String, String> plan;
|
||||
for (int i = 0; i < 20; i++) {
|
||||
plan = StateMachineTestPlanBuilder.<String, String> builder()
|
||||
.defaultAwaitTime(1)
|
||||
.stateMachine(buildMachine())
|
||||
.step()
|
||||
.expectStateMachineStarted(1)
|
||||
.expectStateEntered(1)
|
||||
.expectStateEntered("initial")
|
||||
.and()
|
||||
.step()
|
||||
.sendEvent("repeate")
|
||||
.expectStates("initial")
|
||||
.and()
|
||||
.step()
|
||||
.expectStateEntered(1)
|
||||
.expectStateEntered("end")
|
||||
.and()
|
||||
.step()
|
||||
.expectStateMachineStopped(1)
|
||||
.and()
|
||||
.build();
|
||||
plan.test();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,6 +116,7 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
|
||||
private volatile Message<E> forwardedInitialEvent;
|
||||
|
||||
private final Object lock = new Object();
|
||||
private final Object lock2 = new Object();
|
||||
|
||||
private StateMachine<S, E> parentMachine;
|
||||
|
||||
@@ -211,30 +212,9 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
|
||||
|
||||
@Override
|
||||
public boolean sendEvent(Message<E> event) {
|
||||
if (hasStateMachineError()) {
|
||||
// TODO: should we throw exception?
|
||||
notifyEventNotAccepted(buildStateContext(Stage.EVENT_NOT_ACCEPTED, event, null, getRelayStateMachine(), getState(), null));
|
||||
return false;
|
||||
synchronized (lock2) {
|
||||
return sendEventInternal(event);
|
||||
}
|
||||
|
||||
try {
|
||||
event = getStateMachineInterceptors().preEvent(event, this);
|
||||
} catch (Exception e) {
|
||||
log.info("Event " + event + " threw exception in interceptors, not accepting event");
|
||||
notifyEventNotAccepted(buildStateContext(Stage.EVENT_NOT_ACCEPTED, event, null, getRelayStateMachine(), getState(), null));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isComplete() || !isRunning()) {
|
||||
notifyEventNotAccepted(buildStateContext(Stage.EVENT_NOT_ACCEPTED, event, null, getRelayStateMachine(), getState(), null));
|
||||
return false;
|
||||
}
|
||||
boolean accepted = acceptEvent(event);
|
||||
stateMachineExecutor.execute();
|
||||
if (!accepted) {
|
||||
notifyEventNotAccepted(buildStateContext(Stage.EVENT_NOT_ACCEPTED, event, null, getRelayStateMachine(), getState(), null));
|
||||
}
|
||||
return accepted;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -555,6 +535,33 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
|
||||
forwardedInitialEvent = message;
|
||||
}
|
||||
|
||||
private boolean sendEventInternal(Message<E> event) {
|
||||
if (hasStateMachineError()) {
|
||||
// TODO: should we throw exception?
|
||||
notifyEventNotAccepted(buildStateContext(Stage.EVENT_NOT_ACCEPTED, event, null, getRelayStateMachine(), getState(), null));
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
event = getStateMachineInterceptors().preEvent(event, this);
|
||||
} catch (Exception e) {
|
||||
log.info("Event " + event + " threw exception in interceptors, not accepting event");
|
||||
notifyEventNotAccepted(buildStateContext(Stage.EVENT_NOT_ACCEPTED, event, null, getRelayStateMachine(), getState(), null));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isComplete() || !isRunning()) {
|
||||
notifyEventNotAccepted(buildStateContext(Stage.EVENT_NOT_ACCEPTED, event, null, getRelayStateMachine(), getState(), null));
|
||||
return false;
|
||||
}
|
||||
boolean accepted = acceptEvent(event);
|
||||
stateMachineExecutor.execute();
|
||||
if (!accepted) {
|
||||
notifyEventNotAccepted(buildStateContext(Stage.EVENT_NOT_ACCEPTED, event, null, getRelayStateMachine(), getState(), null));
|
||||
}
|
||||
return accepted;
|
||||
}
|
||||
|
||||
private StateMachine<S, E> getRelayStateMachine() {
|
||||
return relay != null ? relay : this;
|
||||
}
|
||||
@@ -758,7 +765,7 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
|
||||
State<S,E> source = transition.getSource();
|
||||
Trigger<S, E> trigger = transition.getTrigger();
|
||||
|
||||
if (StateMachineUtils.containsAtleastOne(source.getIds(), currentState.getIds())) {
|
||||
if (currentState != null && StateMachineUtils.containsAtleastOne(source.getIds(), currentState.getIds())) {
|
||||
if (trigger != null && trigger.evaluate(new DefaultTriggerContext<S, E>(message.getPayload()))) {
|
||||
stateMachineExecutor.queueEvent(message);
|
||||
return true;
|
||||
@@ -924,11 +931,18 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
|
||||
return null;
|
||||
}
|
||||
|
||||
synchronized void setCurrentState(State<S, E> state, Message<E> message, Transition<S, E> transition, boolean exit, StateMachine<S, E> stateMachine) {
|
||||
void setCurrentState(State<S, E> state, Message<E> message, Transition<S, E> transition, boolean exit, StateMachine<S, E> stateMachine) {
|
||||
setCurrentState(state, message, transition, exit, stateMachine, null, null);
|
||||
}
|
||||
|
||||
synchronized void setCurrentState(State<S, E> state, Message<E> message, Transition<S, E> transition, boolean exit,
|
||||
void setCurrentState(State<S, E> state, Message<E> message, Transition<S, E> transition, boolean exit,
|
||||
StateMachine<S, E> stateMachine, Collection<State<S, E>> sources, Collection<State<S, E>> targets) {
|
||||
synchronized (lock2) {
|
||||
setCurrentStateInternal(state, message, transition, exit, stateMachine, sources, targets);
|
||||
}
|
||||
}
|
||||
|
||||
private void setCurrentStateInternal(State<S, E> state, Message<E> message, Transition<S, E> transition, boolean exit,
|
||||
StateMachine<S, E> stateMachine, Collection<State<S, E>> sources, Collection<State<S, E>> targets) {
|
||||
State<S, E> findDeep = findDeepParent(state);
|
||||
boolean isTargetSubOf = false;
|
||||
|
||||
@@ -263,7 +263,7 @@ public class DefaultStateMachineExecutor<S, E> extends LifecycleObjectSupport im
|
||||
stateMachineExecutorTransit.transit(tran, stateContext, queuedMessage);
|
||||
}
|
||||
|
||||
private synchronized void scheduleEventQueueProcessing() {
|
||||
private void scheduleEventQueueProcessing() {
|
||||
TaskExecutor executor = getTaskExecutor();
|
||||
if (executor == null) {
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
* Copyright 2016-2017 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -27,6 +27,8 @@ 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.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.statemachine.AbstractStateMachineTests;
|
||||
import org.springframework.statemachine.StateContext;
|
||||
import org.springframework.statemachine.StateMachine;
|
||||
@@ -53,15 +55,48 @@ public class ActionAndTimerTests extends AbstractStateMachineTests {
|
||||
assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S1));
|
||||
machine.sendEvent(TestEvents.E1);
|
||||
assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S2));
|
||||
// sleep so that action with timerOnce(1000) is fired before event is send
|
||||
// event sending is happening on a main thread, but actions are executed on a same
|
||||
// pool than the DefaultStateMachineExecutor is using. existing action execution is interrupted.
|
||||
Thread.sleep(2000);
|
||||
|
||||
assertThat(testTimerAction.latch.await(4, TimeUnit.SECONDS), is(true));
|
||||
assertThat(testTimerAction.e, nullValue());
|
||||
|
||||
// need to sleep for TimerTrigger not causing
|
||||
// next event to get handled with threads, thus
|
||||
// causing interrupt
|
||||
Thread.sleep(1000);
|
||||
|
||||
machine.sendEvent(TestEvents.E2);
|
||||
assertThat(testListener.s3EnteredLatch.await(2, TimeUnit.SECONDS), is(true));
|
||||
assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S3));
|
||||
assertThat(testTimerAction.latch.await(2, TimeUnit.SECONDS), is(true));
|
||||
assertThat(testExitAction.latch.await(2, TimeUnit.SECONDS), is(true));
|
||||
assertThat(testExitAction.e, nullValue());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testExitActionWithTimerOnceThreadPoolTaskScheduler() throws Exception {
|
||||
context.register(Config2.class);
|
||||
context.refresh();
|
||||
StateMachine<TestStates, TestEvents> machine = context.getBean(StateMachine.class);
|
||||
TestTimerAction testTimerAction = context.getBean(TestTimerAction.class);
|
||||
TestExitAction testExitAction = context.getBean(TestExitAction.class);
|
||||
TestListener testListener = new TestListener();
|
||||
machine.addStateListener(testListener);
|
||||
machine.start();
|
||||
assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S1));
|
||||
machine.sendEvent(TestEvents.E1);
|
||||
assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S2));
|
||||
|
||||
assertThat(testTimerAction.latch.await(4, TimeUnit.SECONDS), is(true));
|
||||
assertThat(testTimerAction.e, nullValue());
|
||||
|
||||
// need to sleep for TimerTrigger not causing
|
||||
// next event to get handled with threads, thus
|
||||
// causing interrupt
|
||||
Thread.sleep(1000);
|
||||
|
||||
machine.sendEvent(TestEvents.E2);
|
||||
assertThat(testListener.s3EnteredLatch.await(2, TimeUnit.SECONDS), is(true));
|
||||
assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S3));
|
||||
assertThat(testExitAction.latch.await(2, TimeUnit.SECONDS), is(true));
|
||||
assertThat(testExitAction.e, nullValue());
|
||||
}
|
||||
@@ -109,6 +144,55 @@ public class ActionAndTimerTests extends AbstractStateMachineTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine
|
||||
static class Config2 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(TestStates.S1)
|
||||
.state(TestStates.S2, null, testExitAction())
|
||||
.state(TestStates.S3);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> 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)
|
||||
.and()
|
||||
.withInternal()
|
||||
.source(TestStates.S2)
|
||||
.action(testTimerAction())
|
||||
.timerOnce(1000);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TaskScheduler taskScheduler() {
|
||||
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
|
||||
return taskScheduler;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestExitAction testExitAction() {
|
||||
return new TestExitAction();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestTimerAction testTimerAction() {
|
||||
return new TestTimerAction();
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestListener extends StateMachineListenerAdapter<TestStates, TestEvents> {
|
||||
|
||||
volatile CountDownLatch s3EnteredLatch = new CountDownLatch(1);
|
||||
|
||||
Reference in New Issue
Block a user