diff --git a/docs/src/reference/asciidoc/sm-examples.adoc b/docs/src/reference/asciidoc/sm-examples.adoc index a0d5e363..9901d8a2 100644 --- a/docs/src/reference/asciidoc/sm-examples.adoc +++ b/docs/src/reference/asciidoc/sm-examples.adoc @@ -1184,9 +1184,12 @@ include::samples/demo/eventservice/StateMachineConfig.java[tags=snippetC] ---- In below config we setup a `RedisConnectionFactory` which defaults to -localhost and default ports. We also use `StateMachinePersist` with -`RepositoryStateMachinePersist` implementation. These are used -in a `Controller` handling `REST` calls. +localhost and default port. We use `StateMachinePersist` with a +`RepositoryStateMachinePersist` implementation. Finally we create a +`RedisStateMachinePersister` which underneath uses a previously +created `StateMachinePersist` bean. + +These are then used in a `Controller` handling `REST` calls. [source,java,indent=0] ---- @@ -1258,14 +1261,14 @@ include::samples/demo/eventservice/StateMachineController.java[tags=snippetC] ---- Below `feedMachine` will send event into a `StateMachine` and persists -its state using a `StateMachinePersist`. +its state using a `StateMachinePersister`. [source,java,indent=0] ---- include::samples/demo/eventservice/StateMachineController.java[tags=snippetD] ---- -Below `resetStateMachineFromStore` is used to reset a state machine +Below `resetStateMachineFromStore` is used to restore a state machine for a particular user. [source,java,indent=0] diff --git a/docs/src/reference/asciidoc/sm.adoc b/docs/src/reference/asciidoc/sm.adoc index 253740fb..4b63950b 100644 --- a/docs/src/reference/asciidoc/sm.adoc +++ b/docs/src/reference/asciidoc/sm.adoc @@ -1429,6 +1429,57 @@ state change attempt will be halted and instead of ending into an inconsistent state, user can then handle this error manually. Using the interceptors are discussed in <>. +[[sm-persist-statemachinecontext]] +=== Using StateMachineContext +It is impossible to persist a _StateMachine_ using normal java +serialization as object graph is too rich and contains too much +dependencies into other Spring context classes. `StateMachineContext` +is a runtime representation of a state machine which can be used to +restore an existing machine into a state represented by a particular +`StateMachineContext` object. + +[[sm-persist-statemachinepersister]] +=== Using StateMachinePersister +Building a `StateMachineContext` and then restoring a state machine +from it has always been a little bit of a black magic if done +manually. Interface `StateMachinePersister` aims to ease these +operations by providing _persist_ and _restore_ methods. Default +implementation of this interface is `DefaultStateMachinePersister` + +Usage of a `StateMachinePersister` is easy to demonstrate by following +a snippets from tests. We start by creating to two similar configs for +a state machine `machine1` and `machine2`. We could build different +machines for this demonstration using various other ways but this +servers a purpose for this case. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests5.java[tags=snippetA] +---- + +As we're using a `StateMachinePersist` we simply create an in-memory +implementation. + +[NOTE] +==== +In-memory sample is just for demostration purposes, use a real +persistent storage implementations. +==== + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests5.java[tags=snippetB] +---- + +After we have instantiated two different machines we can transfer +`machine1` into state `S2` via event `E1`, then persist it and restore +`machine2`. + +[source,java,indent=0] +---- +include::samples/DocsConfigurationSampleTests5.java[tags=snippetC] +---- + [[sm-persist-redis]] === Using Redis Support for persisting State Machine into Redis is done via @@ -1437,6 +1488,10 @@ Support for persisting State Machine into Redis is done via `RedisStateMachineContextRepository` whic uses `kryo` serialization to persist a `StateMachineContext` into `Redis`. +For `StateMachinePersister` we have a redis related +`RedisStateMachinePersister` implementation which takes an instance of +a `StateMachinePersist` and uses _String_ as its context object. + [TIP] ==== Check sample <> for detailed usage. diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractStateMachinePersister.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractStateMachinePersister.java index 000900b4..a5ab1d73 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractStateMachinePersister.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/AbstractStateMachinePersister.java @@ -33,7 +33,7 @@ import org.springframework.util.Assert; /** * Base implementation of a {@link StateMachinePersister} easing persist - * operations with a {@link StateMachinePersist}. + * and restore operations with a {@link StateMachinePersist}. * * @author Janne Valkealahti * @@ -61,7 +61,7 @@ public abstract class AbstractStateMachinePersister implements StateMac } @Override - public final StateMachine reset(StateMachine stateMachine, T contextOjb) throws Exception { + public final StateMachine restore(StateMachine stateMachine, T contextOjb) throws Exception { final StateMachineContext context = stateMachinePersist.read(contextOjb); stateMachine.stop(); stateMachine.getStateMachineAccessor().doWithAllRegions(new StateMachineFunction>() { diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/StateMachinePersister.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/StateMachinePersister.java index f99d0911..9e829d1b 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/StateMachinePersister.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/persist/StateMachinePersister.java @@ -18,7 +18,8 @@ package org.springframework.statemachine.persist; import org.springframework.statemachine.StateMachine; /** - * Interface persisting a {@link StateMachine}. + * Interface persisting and restoring a {@link StateMachine} from + * a persistent storage. * * @author Janne Valkealahti * @@ -46,5 +47,5 @@ public interface StateMachinePersister { * @return the state machine * @throws Exception the exception in case or any persist error */ - StateMachine reset(StateMachine stateMachine, T contextOjb) throws Exception; + StateMachine restore(StateMachine stateMachine, T contextOjb) throws Exception; } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests5.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests5.java new file mode 100644 index 00000000..5d6179a6 --- /dev/null +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/docs/DocsConfigurationSampleTests5.java @@ -0,0 +1,118 @@ +/* + * 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.docs; + +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.assertThat; + +import java.util.HashMap; + +import org.junit.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.statemachine.AbstractStateMachineTests; +import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.StateMachineContext; +import org.springframework.statemachine.StateMachinePersist; +import org.springframework.statemachine.StateMachineSystemConstants; +import org.springframework.statemachine.config.EnableStateMachine; +import org.springframework.statemachine.config.StateMachineConfigurerAdapter; +import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; +import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; +import org.springframework.statemachine.persist.DefaultStateMachinePersister; +import org.springframework.statemachine.persist.StateMachinePersister; + +public class DocsConfigurationSampleTests5 extends AbstractStateMachineTests { + + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + + + @SuppressWarnings("unchecked") + @Test + public void testPersist() throws Exception { + context.register(Config1.class, Config2.class); + context.refresh(); + +// tag::snippetC[] + InMemoryStateMachinePersist stateMachinePersist = new InMemoryStateMachinePersist(); + StateMachinePersister persister = new DefaultStateMachinePersister<>(stateMachinePersist); + + StateMachine stateMachine1 = context.getBean("machine1", StateMachine.class); + StateMachine stateMachine2 = context.getBean("machine2", StateMachine.class); + stateMachine1.start(); + + stateMachine1.sendEvent("E1"); + assertThat(stateMachine1.getState().getIds(), contains("S2")); + + persister.persist(stateMachine1, "myid"); + persister.restore(stateMachine2, "myid"); + assertThat(stateMachine2.getState().getIds(), contains("S2")); +// end::snippetC[] + } + +// tag::snippetA[] + @Configuration + @EnableStateMachine(name = "machine1") + static class Config1 extends Config { + } + + @Configuration + @EnableStateMachine(name = "machine2") + static class Config2 extends Config { + } + + static class Config extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S1") + .state("S1") + .state("S2"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S1") + .target("S2") + .event("E1"); + } + } +// end::snippetA[] + +// tag::snippetB[] + static class InMemoryStateMachinePersist implements StateMachinePersist { + + private final HashMap> contexts = new HashMap<>(); + + @Override + public void write(StateMachineContext context, String contextOjb) throws Exception { + contexts.put(contextOjb, context); + } + + @Override + public StateMachineContext read(String contextOjb) throws Exception { + return contexts.get(contextOjb); + } + } +// end::snippetB[] +} diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/persist/StateMachinePersistTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/persist/StateMachinePersistTests.java index e117518e..47965cd1 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/persist/StateMachinePersistTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/persist/StateMachinePersistTests.java @@ -56,7 +56,7 @@ public class StateMachinePersistTests extends AbstractStateMachineTests { StateMachinePersister persister = new DefaultStateMachinePersister<>(stateMachinePersist); persister.persist(stateMachine, "xxx"); - persister.reset(stateMachine, "xxx"); + persister.restore(stateMachine, "xxx"); assertThat(stateMachine.getState().getIds(), contains("S2")); stateMachine.sendEvent("E2"); @@ -77,7 +77,7 @@ public class StateMachinePersistTests extends AbstractStateMachineTests { StateMachinePersister persister = new DefaultStateMachinePersister<>(stateMachinePersist); persister.persist(stateMachine, "xxx"); - persister.reset(stateMachine, "xxx"); + persister.restore(stateMachine, "xxx"); assertThat(stateMachine.getState().getIds(), contains(TestStates.S2)); stateMachine.sendEvent(TestEvents.E2); @@ -99,7 +99,7 @@ public class StateMachinePersistTests extends AbstractStateMachineTests { persister.persist(stateMachine, "xxx"); stateMachine.getExtendedState().getVariables().remove("foo"); - stateMachine = persister.reset(stateMachine, "xxx"); + stateMachine = persister.restore(stateMachine, "xxx"); assertThat(stateMachine.getExtendedState().get("foo", String.class), is("bar")); } @@ -120,13 +120,13 @@ public class StateMachinePersistTests extends AbstractStateMachineTests { StateMachinePersister persister = new DefaultStateMachinePersister<>(stateMachinePersist); persister.persist(stateMachine, "xxx"); - stateMachine = persister.reset(stateMachine, "xxx"); + stateMachine = persister.restore(stateMachine, "xxx"); assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S22")); stateMachine.sendEvent("E2"); assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S3", "S31")); - stateMachine = persister.reset(stateMachine, "xxx"); + stateMachine = persister.restore(stateMachine, "xxx"); assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S2", "S22")); } @@ -151,7 +151,7 @@ public class StateMachinePersistTests extends AbstractStateMachineTests { StateMachinePersister persister = new DefaultStateMachinePersister<>(stateMachinePersist); persister.persist(stateMachine, "xxx"); - stateMachine = persister.reset(stateMachine, "xxx"); + stateMachine = persister.restore(stateMachine, "xxx"); assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S12", "S22", "S32")); stateMachine.sendEvent("E4"); @@ -159,10 +159,43 @@ public class StateMachinePersistTests extends AbstractStateMachineTests { stateMachine.sendEvent("E6"); assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S13", "S23", "S33")); - stateMachine = persister.reset(stateMachine, "xxx"); + stateMachine = persister.restore(stateMachine, "xxx"); assertThat(stateMachine.getState().getIds(), containsInAnyOrder("S12", "S22", "S32")); } + @SuppressWarnings("unchecked") + @Test + public void testSubsInRegions() throws Exception { + context.register(Config51.class, Config52.class); + context.refresh(); + + InMemoryStateMachinePersist1 stateMachinePersist = new InMemoryStateMachinePersist1(); + StateMachinePersister persister = new DefaultStateMachinePersister<>(stateMachinePersist); + + StateMachine stateMachine1 = context.getBean("machine1", StateMachine.class); + StateMachine stateMachine2 = context.getBean("machine2", StateMachine.class); + stateMachine1.start(); + + assertThat(stateMachine1.getState().getIds(), containsInAnyOrder("S11", "S111", "S21")); + persister.persist(stateMachine1, "xxx"); + stateMachine2 = persister.restore(stateMachine2, "xxx"); + assertThat(stateMachine2.getState().getIds(), containsInAnyOrder("S11", "S111", "S21")); + + stateMachine1.sendEvent("E1"); + assertThat(stateMachine1.getState().getIds(), containsInAnyOrder("S12", "S21")); + persister.persist(stateMachine1, "xxx"); + assertThat(stateMachine2.getState().getIds(), containsInAnyOrder("S11", "S111", "S21")); + stateMachine2 = persister.restore(stateMachine2, "xxx"); + assertThat(stateMachine2.getState().getIds(), containsInAnyOrder("S12", "S21")); + + stateMachine1.sendEvent("E2"); + assertThat(stateMachine1.getState().getIds(), containsInAnyOrder("S12", "S22", "S221")); + persister.persist(stateMachine1, "xxx"); + assertThat(stateMachine2.getState().getIds(), containsInAnyOrder("S12", "S21")); + stateMachine2 = persister.restore(stateMachine2, "xxx"); + assertThat(stateMachine2.getState().getIds(), containsInAnyOrder("S12", "S22", "S221")); + } + @Configuration @EnableStateMachine static class Config1 extends StateMachineConfigurerAdapter { @@ -328,6 +361,62 @@ public class StateMachinePersistTests extends AbstractStateMachineTests { } } + static class Config5 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("S11") + .state("S11") + .state("S12") + .state("S13") + .and() + .withStates() + .parent("S11") + .initial("S111") + .state("S111") + .state("S112") + .and() + .withStates() + .initial("S21") + .state("S21") + .state("S22") + .state("S23") + .and() + .withStates() + .parent("S22") + .initial("S221") + .state("S221") + .state("S222"); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("S11") + .target("S12") + .event("E1") + .and() + .withExternal() + .source("S21") + .target("S22") + .event("E2"); + } + + } + + @Configuration + @EnableStateMachine(name = "machine1") + static class Config51 extends Config5 { + } + + @Configuration + @EnableStateMachine(name = "machine2") + static class Config52 extends Config5 { + } + static class InMemoryStateMachinePersist1 implements StateMachinePersist { private final HashMap> contexts = new HashMap<>(); diff --git a/spring-statemachine-redis/src/main/java/org/springframework/statemachine/redis/RedisStateMachinePersister.java b/spring-statemachine-redis/src/main/java/org/springframework/statemachine/redis/RedisStateMachinePersister.java new file mode 100644 index 00000000..ded32fb6 --- /dev/null +++ b/spring-statemachine-redis/src/main/java/org/springframework/statemachine/redis/RedisStateMachinePersister.java @@ -0,0 +1,40 @@ +/* + * 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.redis; + +import org.springframework.statemachine.StateMachinePersist; +import org.springframework.statemachine.persist.AbstractStateMachinePersister; +import org.springframework.statemachine.persist.StateMachinePersister; + +/** + * Implementation of a {@link StateMachinePersister} to be used with a redis. + * + * @author Janne Valkealahti + * + * @param the type of state + * @param the type of event + */ +public class RedisStateMachinePersister extends AbstractStateMachinePersister { + + /** + * Instantiates a new redis state machine persister. + * + * @param stateMachinePersist the state machine persist + */ + public RedisStateMachinePersister(StateMachinePersist stateMachinePersist) { + super(stateMachinePersist); + } +} diff --git a/spring-statemachine-samples/eventservice/src/main/java/demo/eventservice/StateMachineConfig.java b/spring-statemachine-samples/eventservice/src/main/java/demo/eventservice/StateMachineConfig.java index 0d11663b..f46c4a4e 100644 --- a/spring-statemachine-samples/eventservice/src/main/java/demo/eventservice/StateMachineConfig.java +++ b/spring-statemachine-samples/eventservice/src/main/java/demo/eventservice/StateMachineConfig.java @@ -37,6 +37,7 @@ import org.springframework.statemachine.config.StateMachineBuilder; import org.springframework.statemachine.config.StateMachineBuilder.Builder; import org.springframework.statemachine.persist.RepositoryStateMachinePersist; import org.springframework.statemachine.redis.RedisStateMachineContextRepository; +import org.springframework.statemachine.redis.RedisStateMachinePersister; @Configuration public class StateMachineConfig { @@ -214,6 +215,12 @@ public class StateMachineConfig { new RedisStateMachineContextRepository(connectionFactory); return new RepositoryStateMachinePersist(repository); } + + @Bean + public RedisStateMachinePersister redisStateMachinePersister( + StateMachinePersist stateMachinePersist) { + return new RedisStateMachinePersister(stateMachinePersist); + } //end::snippetD[] @Bean diff --git a/spring-statemachine-samples/eventservice/src/main/java/demo/eventservice/StateMachineController.java b/spring-statemachine-samples/eventservice/src/main/java/demo/eventservice/StateMachineController.java index 71b4a864..f4013368 100644 --- a/spring-statemachine-samples/eventservice/src/main/java/demo/eventservice/StateMachineController.java +++ b/spring-statemachine-samples/eventservice/src/main/java/demo/eventservice/StateMachineController.java @@ -18,11 +18,7 @@ package demo.eventservice; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.statemachine.StateMachine; -import org.springframework.statemachine.StateMachineContext; -import org.springframework.statemachine.StateMachinePersist; -import org.springframework.statemachine.access.StateMachineAccess; -import org.springframework.statemachine.access.StateMachineFunction; -import org.springframework.statemachine.support.DefaultStateMachineContext; +import org.springframework.statemachine.persist.StateMachinePersister; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.util.Assert; @@ -44,7 +40,7 @@ public class StateMachineController { private StateMachine stateMachine; @Autowired - private StateMachinePersist stateMachinePersist; + private StateMachinePersister stateMachinePersister; //end::snippetA[] @Autowired @@ -90,25 +86,13 @@ public class StateMachineController { //tag::snippetD[] private void feedMachine(String user, Events id) throws Exception { stateMachine.sendEvent(id); - stateMachinePersist.write(new DefaultStateMachineContext(stateMachine.getState().getId(), null, null, - stateMachine.getExtendedState()), "testprefix:" + user); + stateMachinePersister.persist(stateMachine, "testprefix:" + user); } //end::snippetD[] //tag::snippetE[] private StateMachine resetStateMachineFromStore(String user) throws Exception { - final StateMachineContext context = stateMachinePersist.read("testprefix:" + user); - stateMachine.stop(); - stateMachine.getStateMachineAccessor() - .doWithAllRegions(new StateMachineFunction>() { - - @Override - public void apply(StateMachineAccess function) { - function.resetStateMachine(context); - } - }); - stateMachine.start(); - return stateMachine; + return stateMachinePersister.restore(stateMachine, "testprefix:" + user); } //end::snippetE[] }