From 8cb5a81652d1d777a78a529a7312f1589068edd4 Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Wed, 9 Nov 2016 13:47:56 +0000 Subject: [PATCH] Exit action may get interrupted - Add test case - Synchronise recursive scheduleEventQueueProcessing() in DefaultStateMachineExecutor. - Fixes #233 --- .../support/DefaultStateMachineExecutor.java | 2 +- .../action/ActionAndTimerTests.java | 162 ++++++++++++++++++ 2 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 spring-statemachine-core/src/test/java/org/springframework/statemachine/action/ActionAndTimerTests.java diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java index bbb2277b..28bbea4f 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/DefaultStateMachineExecutor.java @@ -253,7 +253,7 @@ public class DefaultStateMachineExecutor extends LifecycleObjectSupport im stateMachineExecutorTransit.transit(tran, stateContext, queuedMessage); } - private void scheduleEventQueueProcessing() { + private synchronized void scheduleEventQueueProcessing() { TaskExecutor executor = getTaskExecutor(); if (executor == null) { return; diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/action/ActionAndTimerTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/action/ActionAndTimerTests.java new file mode 100644 index 00000000..177fd753 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/action/ActionAndTimerTests.java @@ -0,0 +1,162 @@ +/* + * 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.action; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +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.statemachine.AbstractStateMachineTests; +import org.springframework.statemachine.StateContext; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.config.EnableStateMachine; +import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; +import org.springframework.statemachine.listener.StateMachineListenerAdapter; +import org.springframework.statemachine.state.State; + +public class ActionAndTimerTests extends AbstractStateMachineTests { + + @SuppressWarnings("unchecked") + @Test + public void testExitActionWithTimerOnce() throws Exception { + context.register(Config1.class); + context.refresh(); + StateMachine 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)); + // 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); + 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(testTimerAction.e, nullValue()); + assertThat(testExitAction.latch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(testExitAction.e, nullValue()); + } + + @Configuration + @EnableStateMachine + static class Config1 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S2, null, testExitAction()) + .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) + .and() + .withInternal() + .source(TestStates.S2) + .action(testTimerAction()) + .timerOnce(1000); + } + + @Bean + public TestExitAction testExitAction() { + return new TestExitAction(); + } + + @Bean + public TestTimerAction testTimerAction() { + return new TestTimerAction(); + } + } + + private static class TestListener extends StateMachineListenerAdapter { + + volatile CountDownLatch s3EnteredLatch = new CountDownLatch(1); + + @Override + public void stateEntered(State state) { + if (state.getId().equals(TestStates.S3)) { + s3EnteredLatch.countDown(); + } + } + } + + private static class TestTimerAction implements Action { + + volatile CountDownLatch latch = new CountDownLatch(1); + volatile Exception e; + + @Override + public void execute(StateContext context) { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + this.e = e; + } finally { + latch.countDown(); + } + } + } + + private static class TestExitAction implements Action { + + volatile CountDownLatch latch = new CountDownLatch(1); + volatile Exception e; + + @Override + public void execute(StateContext context) { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + this.e = e; + } finally { + latch.countDown(); + } + } + } + + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } +}