diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/service/DefaultStateMachineService.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/service/DefaultStateMachineService.java index 352b897c..3bc328f6 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/service/DefaultStateMachineService.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/service/DefaultStateMachineService.java @@ -17,9 +17,11 @@ package org.springframework.statemachine.service; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CountDownLatch; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.context.Lifecycle; import org.springframework.statemachine.StateMachine; import org.springframework.statemachine.StateMachineContext; import org.springframework.statemachine.StateMachineException; @@ -27,6 +29,7 @@ import org.springframework.statemachine.StateMachinePersist; import org.springframework.statemachine.access.StateMachineAccess; import org.springframework.statemachine.access.StateMachineFunction; import org.springframework.statemachine.config.StateMachineFactory; +import org.springframework.statemachine.listener.StateMachineListenerAdapter; import org.springframework.util.Assert; /** @@ -68,6 +71,11 @@ public class DefaultStateMachineService implements StateMachineService acquireStateMachine(String machineId) { + return acquireStateMachine(machineId, true); + } + + @Override + public StateMachine acquireStateMachine(String machineId, boolean start) { log.info("Acquiring machine with id " + machineId); synchronized (machines) { StateMachine stateMachine = machines.get(machineId); @@ -85,7 +93,7 @@ public class DefaultStateMachineService implements StateMachineService implements StateMachineService stateMachine = machines.remove(machineId); + if (stateMachine != null) { + log.info("Found machine with id " + machineId); + handleStop(stateMachine, stop); + } + } + } + /** * Sets the state machine persist. * @@ -124,4 +144,66 @@ public class DefaultStateMachineService implements StateMachineService handleStart(StateMachine stateMachine, boolean start) { + if (start) { + if (!((Lifecycle) stateMachine).isRunning()) { + StartListener listener = new StartListener<>(stateMachine); + stateMachine.addStateListener(listener); + stateMachine.start(); + try { + listener.latch.await(); + } catch (InterruptedException e) { + } + } + } + return stateMachine; + } + + protected StateMachine handleStop(StateMachine stateMachine, boolean stop) { + if (stop) { + if (((Lifecycle) stateMachine).isRunning()) { + StopListener listener = new StopListener<>(stateMachine); + stateMachine.addStateListener(listener); + stateMachine.stop(); + try { + listener.latch.await(); + } catch (InterruptedException e) { + } + } + } + return stateMachine; + } + + private static class StartListener extends StateMachineListenerAdapter { + + final CountDownLatch latch = new CountDownLatch(1); + final StateMachine stateMachine; + + public StartListener(StateMachine stateMachine) { + this.stateMachine = stateMachine; + } + + @Override + public void stateMachineStarted(StateMachine stateMachine) { + this.stateMachine.removeStateListener(this); + latch.countDown(); + } + } + + private static class StopListener extends StateMachineListenerAdapter { + + final CountDownLatch latch = new CountDownLatch(1); + final StateMachine stateMachine; + + public StopListener(StateMachine stateMachine) { + this.stateMachine = stateMachine; + } + + @Override + public void stateMachineStopped(StateMachine stateMachine) { + this.stateMachine.removeStateListener(this); + latch.countDown(); + } + } } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/service/StateMachineService.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/service/StateMachineService.java index d5c7bf8b..6d4475fa 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/service/StateMachineService.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/service/StateMachineService.java @@ -29,17 +29,38 @@ import org.springframework.statemachine.StateMachine; public interface StateMachineService { /** - * Acquires the state machine. + * Acquires the state machine. Machine from this method + * is returned started. * * @param machineId the machine id * @return the state machine + * @see #acquireStateMachine(String, boolean) */ StateMachine acquireStateMachine(String machineId); /** - * Release the state machine. + * Acquires the state machine. * * @param machineId the machine id + * @param start indicating if machine should be returned started + * @return the state machine + */ + StateMachine acquireStateMachine(String machineId, boolean start); + + /** + * Release the state machine. Machine with this method + * is stopped. + * + * @param machineId the machine id + * @see #releaseStateMachine(String, boolean) */ void releaseStateMachine(String machineId); + + /** + * Release state machine. + * + * @param machineId the machine id + * @param stop indicating if machine should be stopped + */ + void releaseStateMachine(String machineId, boolean stop); } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/service/DefaultStateMachineServiceTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/service/DefaultStateMachineServiceTests.java new file mode 100644 index 00000000..a9a95472 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/service/DefaultStateMachineServiceTests.java @@ -0,0 +1,216 @@ +/* + * 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.service; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.context.Lifecycle; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.statemachine.AbstractStateMachineTests; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.StateMachineSystemConstants; +import org.springframework.statemachine.config.EnableStateMachineFactory; +import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; +import org.springframework.statemachine.config.StateMachineFactory; +import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; + +/** + * Tests for {@link DefaultStateMachineService}. + * + * @author Janne Valkealahti + * + */ +@SuppressWarnings("unchecked") +public class DefaultStateMachineServiceTests extends AbstractStateMachineTests { + + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + + @Test + public void testAcquireNotStarted() { + context.register(Config1.class); + context.refresh(); + StateMachineFactory stateMachineFactory = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINEFACTORY, StateMachineFactory.class); + + DefaultStateMachineService service = new DefaultStateMachineService<>(stateMachineFactory); + StateMachine machine1 = service.acquireStateMachine("m1", false); + assertThat(((Lifecycle)machine1).isRunning(), is(false)); + } + + @Test + public void testAcquireStarted() { + context.register(Config1.class); + context.refresh(); + StateMachineFactory stateMachineFactory = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINEFACTORY, StateMachineFactory.class); + + DefaultStateMachineService service = new DefaultStateMachineService<>(stateMachineFactory); + StateMachine machine1 = service.acquireStateMachine("m1", true); + assertThat(((Lifecycle)machine1).isRunning(), is(true)); + } + + @Test + public void testReleaseStopMachine() { + context.register(Config1.class); + context.refresh(); + StateMachineFactory stateMachineFactory = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINEFACTORY, StateMachineFactory.class); + + DefaultStateMachineService service = new DefaultStateMachineService<>(stateMachineFactory); + StateMachine machine1 = service.acquireStateMachine("m1", true); + assertThat(((Lifecycle)machine1).isRunning(), is(true)); + service.releaseStateMachine("m1"); + assertThat(((Lifecycle)machine1).isRunning(), is(false)); + } + + @Test + public void testReleaseDoesNotStopMachine() { + context.register(Config1.class); + context.refresh(); + StateMachineFactory stateMachineFactory = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINEFACTORY, StateMachineFactory.class); + + DefaultStateMachineService service = new DefaultStateMachineService<>(stateMachineFactory); + StateMachine machine1 = service.acquireStateMachine("m1", true); + assertThat(((Lifecycle)machine1).isRunning(), is(true)); + service.releaseStateMachine("m1", false); + assertThat(((Lifecycle)machine1).isRunning(), is(true)); + } + + @Test + public void testAcquireNotStartedThreading() { + context.register(Config2.class); + context.refresh(); + StateMachineFactory stateMachineFactory = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINEFACTORY, StateMachineFactory.class); + + DefaultStateMachineService service = new DefaultStateMachineService<>(stateMachineFactory); + StateMachine machine1 = service.acquireStateMachine("m1", false); + assertThat(((Lifecycle)machine1).isRunning(), is(false)); + } + + @Test + public void testAcquireStartedThreading() { + context.register(Config2.class); + context.refresh(); + StateMachineFactory stateMachineFactory = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINEFACTORY, StateMachineFactory.class); + + DefaultStateMachineService service = new DefaultStateMachineService<>(stateMachineFactory); + StateMachine machine1 = service.acquireStateMachine("m1", true); + assertThat(((Lifecycle)machine1).isRunning(), is(true)); + } + + @Test + public void testReleaseStopsMachineThreading() { + context.register(Config2.class); + context.refresh(); + StateMachineFactory stateMachineFactory = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINEFACTORY, StateMachineFactory.class); + + DefaultStateMachineService service = new DefaultStateMachineService<>(stateMachineFactory); + StateMachine machine1 = service.acquireStateMachine("m1", true); + assertThat(((Lifecycle)machine1).isRunning(), is(true)); + service.releaseStateMachine("m1"); + assertThat(((Lifecycle)machine1).isRunning(), is(false)); + } + + @Test + public void testReleaseDoesNotStopMachineThreading() { + context.register(Config2.class); + context.refresh(); + StateMachineFactory stateMachineFactory = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINEFACTORY, StateMachineFactory.class); + + DefaultStateMachineService service = new DefaultStateMachineService<>(stateMachineFactory); + StateMachine machine1 = service.acquireStateMachine("m1", true); + assertThat(((Lifecycle)machine1).isRunning(), is(true)); + service.releaseStateMachine("m1", false); + assertThat(((Lifecycle)machine1).isRunning(), is(true)); + } + + @Configuration + @EnableStateMachineFactory + static class Config1 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S1) + .state(TestStates.S2); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S2) + .event(TestEvents.E1); + } + } + + @Configuration + @EnableStateMachineFactory + static class Config2 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineConfigurationConfigurer config) throws Exception { + config + .withConfiguration() + .taskExecutor(stateMachineTaskExecutor()); + } + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S1) + .state(TestStates.S2); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source(TestStates.S1) + .target(TestStates.S2) + .event(TestEvents.E1); + } + + @Bean + public TaskExecutor stateMachineTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(4); + return executor; + } + + } +}