diff --git a/build.gradle b/build.gradle index ab82d93f..965efbaa 100644 --- a/build.gradle +++ b/build.gradle @@ -306,6 +306,7 @@ configure(rootProject) { from 'spring-statemachine-samples/persist/src/main/java/' from 'spring-statemachine-samples/zookeeper/src/main/java/' from 'spring-statemachine-samples/security/src/main/java/' + from 'spring-statemachine-samples/eventservice/src/main/java/' include '**/*.java' into 'docs/src/reference/asciidoc/samples' } diff --git a/docs/src/reference/asciidoc/getting-started.adoc b/docs/src/reference/asciidoc/getting-started.adoc index a4faa76d..c607732f 100644 --- a/docs/src/reference/asciidoc/getting-started.adoc +++ b/docs/src/reference/asciidoc/getting-started.adoc @@ -31,6 +31,12 @@ The following modules are available for Spring Statemachine. |Common recipes which doesn't require dependencies outside of a core framework. +|spring-statemachine-kryo +|`Kryo` serializers for state machine. + +|spring-statemachine-redis +|`Redis` related features for state machine. + |spring-statemachine-zookeeper |`Zookeeper` integration for a distributed state machine. diff --git a/docs/src/reference/asciidoc/images/sm-eventservice-1.png b/docs/src/reference/asciidoc/images/sm-eventservice-1.png new file mode 100644 index 00000000..27f58377 Binary files /dev/null and b/docs/src/reference/asciidoc/images/sm-eventservice-1.png differ diff --git a/docs/src/reference/asciidoc/images/sm-eventservice-2.png b/docs/src/reference/asciidoc/images/sm-eventservice-2.png new file mode 100644 index 00000000..ddb87447 Binary files /dev/null and b/docs/src/reference/asciidoc/images/sm-eventservice-2.png differ diff --git a/docs/src/reference/asciidoc/images/sm-eventservice-3.png b/docs/src/reference/asciidoc/images/sm-eventservice-3.png new file mode 100644 index 00000000..e8636d35 Binary files /dev/null and b/docs/src/reference/asciidoc/images/sm-eventservice-3.png differ diff --git a/docs/src/reference/asciidoc/images/sm-eventservice-4.png b/docs/src/reference/asciidoc/images/sm-eventservice-4.png new file mode 100644 index 00000000..329c8e91 Binary files /dev/null and b/docs/src/reference/asciidoc/images/sm-eventservice-4.png differ diff --git a/docs/src/reference/asciidoc/images/statechart14.png b/docs/src/reference/asciidoc/images/statechart14.png new file mode 100644 index 00000000..182ba48a Binary files /dev/null and b/docs/src/reference/asciidoc/images/statechart14.png differ diff --git a/docs/src/reference/asciidoc/index.adoc b/docs/src/reference/asciidoc/index.adoc index 155cd356..37eea9c0 100644 --- a/docs/src/reference/asciidoc/index.adoc +++ b/docs/src/reference/asciidoc/index.adoc @@ -17,8 +17,8 @@ include::preface.adoc[] include::introduction.adoc[] - include::getting-started.adoc[] +include::whatsnew.adoc[] include::sm.adoc[] include::recipes.adoc[] include::sm-examples.adoc[] diff --git a/docs/src/reference/asciidoc/sm-examples.adoc b/docs/src/reference/asciidoc/sm-examples.adoc index e584dbf6..e2e96a81 100644 --- a/docs/src/reference/asciidoc/sm-examples.adoc +++ b/docs/src/reference/asciidoc/sm-examples.adoc @@ -30,6 +30,8 @@ normal build cycle. Samples in this chapter are: <> Security. +<> Event Service. + [source,text] ---- @@ -1131,3 +1133,184 @@ does not hate that role. include::samples/demo/security/StateMachineConfig.java[tags=snippetD] ---- +[[statemachine-examples-eventservice]] +== Event Service +Event Service is an example how state machine concepts can be used as +a processing engine for events. Sample was born out from a question: + +[NOTE] +==== +Can Spring Statemachine be used as a microservice to feed events to it +with millions different state machine instances. +==== + +In this example we will use a `Redis` to persist a state machine +instances. + +Obviously a million state machine instances in a jvm would be +a relatively bad idea due to memory constraints. This simply leads to +other available features from a Spring Statemachine to persist a +`StateMachineContext` and re-use existing instances. + +We assume few things like there is a shopping application which is +sending different types of `PageView` events into a separate +microservice which is then tracking user behaviour using a state +machine. State model is shown below which simply have few states +representing user navigating on product items list, add and remote +items from a cart and going to a payment page and initiating a pay +operation. Actual shopping application would send these events into +this service for example using a simple rest calls. More about this +later. + +[NOTE] +==== +Remember that focus here is to have an application which is exposing a +`REST` api user can use to send events which would be processed by a +state machine per request. +==== + +image::images/statechart14.png[width=500] + +In below state machine configuration we simply model what we have in a +state chart. Various actions are updating state machine `Extended +State` to track number of entry's into various states and also how +many times internal transition for `ADD` and `DEL` are called and if +`PAY` has been executed. Don't focus on `stateMachineTarget` or +`@Scope` for now, as we'll explain those in a bit. + +[source,java,indent=0] +---- +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 then later +in a `Controller` handling `REST` calls. + +[source,java,indent=0] +---- +include::samples/demo/eventservice/StateMachineConfig.java[tags=snippetD] +---- + +We now get into why `StateMachine` was created as `stateMachineTarget` +and a `prototype` bean. State machine instantiation is a relatively +expensive operation so it is better to try to pool instances instead +of instantiating a new instance with every request. For this we first +create a `poolTargetSource` which wraps `stateMachineTarget` and pools +it with max size of 3. This `poolTargetSource` is then proxied with +`ProxyFactoryBean` using a `request` scope. Effectively this means +that every `REST` request will get pooled state machine instance from +a bean factory. It's shown later how these are used. + +[source,java,indent=0] +---- +include::samples/demo/eventservice/StateMachineConfig.java[tags=snippetA] +---- + +[source,java,indent=0] +---- +include::samples/demo/eventservice/StateMachineConfig.java[tags=snippetB] +---- + +Lets get into actual demo. You need to have a redis running on a +localhost with a default settings. Then run the boot based sample +application: + +[source,text,subs="attributes"] +---- +@n1:~# java -jar spring-statemachine-samples-eventservice-{revnumber}.jar +---- + +In a browser you see something like: + +image::images/sm-eventservice-1.png[width=500] + +In this UI you have three users you can use, `joe`, `bob` and `dave`. +Clicking button will show current state and extended state. Enabling a +radio button before clicking users will send particular event for that +user. This is a way you can play with this using an UI. + +In our `StateMachineController` we autowire `StateMachine` and +`StateMachinePersist`. `StateMachine` is a `request` scoped so you'll +get new instance per request while `StateMachinePersist` is normal +singleton bean. + +[source,java,indent=0] +---- +include::samples/demo/eventservice/StateMachineController.java[tags=snippetA] +---- + +Below `feedAndGetState` is just used with an UI to do same things what +actual `REST` api will do. + +[source,java,indent=0] +---- +include::samples/demo/eventservice/StateMachineController.java[tags=snippetB] +---- + +Below `feedPageview` is a `REST` method which accepts a post with a +json content. + +[source,java,indent=0] +---- +include::samples/demo/eventservice/StateMachineController.java[tags=snippetC] +---- + +Below `feedMachine` will send event into a `StateMachine` and persists +its state using a `StateMachinePersist`. + +[source,java,indent=0] +---- +include::samples/demo/eventservice/StateMachineController.java[tags=snippetD] +---- + +Below `resetStateMachineFromStore` is used to reset a state machine +for a particular user. + +[source,java,indent=0] +---- +include::samples/demo/eventservice/StateMachineController.java[tags=snippetE] +---- + +As you'd send event using UI, same can be done using a `REST` calls: + +[source,text] +---- +# curl http://localhost:8080/feed -H "Content-Type: application/json" --data '{"user":"joe","id":"VIEW_I"}' +---- + +At this point you should have a content in `Redis` with a +`testprefix:joe` key. + +[source,text] +---- +$ ./redis-cli +127.0.0.1:6379> KEYS * +1) "testprefix:joe" +---- + +Below is a three images when state for `joe` has been changed from +`HOME` to `ITEMS` and when `ADD` action has been executed. + +Send evend `ADD`: + +image::images/sm-eventservice-2.png[width=500] + +Now your are still on state `ITEMS` and internal transition caused +extended state variable `COUNT` to increase to `1`. + +image::images/sm-eventservice-3.png[width=500] + +Execute below `curl` rest call few times or do it via UI and you +should see `COUNT` variable to increase with every call. + +[source,text] +---- +# curl http://localhost:8080/feed -H "Content-Type: application/json" # --data '{"user":"joe","id":"ADD"}' +---- + +image::images/sm-eventservice-4.png[width=500] + +Order to implement actual logic to track user behaviour, you'd then +define this in a various `Actions` in a state machine. diff --git a/docs/src/reference/asciidoc/sm.adoc b/docs/src/reference/asciidoc/sm.adoc index 888f556d..fd0abd46 100644 --- a/docs/src/reference/asciidoc/sm.adoc +++ b/docs/src/reference/asciidoc/sm.adoc @@ -1421,6 +1421,17 @@ 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-redis]] +=== Using Redis +Support for persisting State Machine into Redis is done via +`RepositoryStateMachinePersist` which implements +`StateMachinePersist`. + +[TIP] +==== +Check sample <> for detailed usage. +==== + [[sm-distributed]] == Using Distributed States Distributed state is probably one of a most compicated concepts of a diff --git a/docs/src/reference/asciidoc/whatsnew.adoc b/docs/src/reference/asciidoc/whatsnew.adoc new file mode 100644 index 00000000..bee29b7f --- /dev/null +++ b/docs/src/reference/asciidoc/whatsnew.adoc @@ -0,0 +1,16 @@ +[[whatsnew]] += What's New + +== In 1.1 +_Spring Statemachine 1.1_ is focusing on security and a better +interoperability with web applications. + +* Comprehensive support for _Spring Security_ is added, <> +* Context integration with `@WithStateMachine' has been greatly + enhanced, <> +* `StateContext` is now a first class citizen with how user can + interact with a State Machine, <>. +* Features around persistence has been enhanced with a build-in + support for redis, <> +* New samples, <>, <> + 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 608602d8..9ddaa240 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 @@ -41,6 +41,7 @@ import org.springframework.statemachine.support.RepositoryStateMachinePersist; @Configuration public class StateMachineConfig { +//tag::snippetA[] @Bean @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) public ProxyFactoryBean stateMachine() { @@ -48,7 +49,9 @@ public class StateMachineConfig { pfb.setTargetSource(poolTargetSource()); return pfb; } +//end::snippetA[] +//tag::snippetB[] @Bean public CommonsPool2TargetSource poolTargetSource() { CommonsPool2TargetSource pool = new CommonsPool2TargetSource(); @@ -56,7 +59,9 @@ public class StateMachineConfig { pool.setTargetBeanName("stateMachineTarget"); return pool; } +//end::snippetB[] +//tag::snippetC[] @Bean(name = "stateMachineTarget") @Scope(scopeName="prototype") public StateMachine stateMachineTarget() throws Exception { @@ -126,6 +131,7 @@ public class StateMachineConfig { return builder.build(); } +//end::snippetC[] @Bean public Action pageviewAction() { @@ -196,6 +202,7 @@ public class StateMachineConfig { }; } +//tag::snippetD[] @Bean public RedisConnectionFactory redisConnectionFactory() { return new JedisConnectionFactory(); @@ -203,8 +210,11 @@ public class StateMachineConfig { @Bean public StateMachinePersist stateMachinePersist(RedisConnectionFactory connectionFactory) { - return new RepositoryStateMachinePersist(new RedisStateMachineContextRepository(connectionFactory)); + RedisStateMachineContextRepository repository = + new RedisStateMachineContextRepository(connectionFactory); + return new RepositoryStateMachinePersist(repository); } + //end::snippetD[] @Bean public String stateChartModel() throws IOException { 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 50a32782..71b4a864 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 @@ -39,11 +39,13 @@ import demo.eventservice.StateMachineConfig.States; @Controller public class StateMachineController { +//tag::snippetA[] @Autowired private StateMachine stateMachine; @Autowired private StateMachinePersist stateMachinePersist; +//end::snippetA[] @Autowired private String stateChartModel; @@ -53,6 +55,7 @@ public class StateMachineController { return "redirect:/state"; } +//tag::snippetB[] @RequestMapping("/state") public String feedAndGetState(@RequestParam(value = "user", required = false) String user, @RequestParam(value = "id", required = false) Events id, Model model) throws Exception { @@ -71,7 +74,9 @@ public class StateMachineController { } return "states"; } +//end::snippetB[] +//tag::snippetC[] @RequestMapping(value = "/feed",method= RequestMethod.POST) @ResponseStatus(HttpStatus.OK) public void feedPageview(@RequestBody(required = true) Pageview event) throws Exception { @@ -80,13 +85,17 @@ public class StateMachineController { resetStateMachineFromStore(event.getUser()); feedMachine(event.getUser(), event.getId()); } +//end::snippetC[] +//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); } +//end::snippetD[] +//tag::snippetE[] private StateMachine resetStateMachineFromStore(String user) throws Exception { final StateMachineContext context = stateMachinePersist.read("testprefix:" + user); stateMachine.stop(); @@ -101,4 +110,5 @@ public class StateMachineController { stateMachine.start(); return stateMachine; } +//end::snippetE[] }