Enhance things around StateMachinePersister

- Polish StateMachinePersister
- Add redis specific RedisStateMachinePersister
- Add more docs.
- Change eventservice sample to use StateMachinePersister.
- Relates #184
- Relates #185
This commit is contained in:
Janne Valkealahti
2016-03-11 18:43:45 +00:00
parent 5cd7d348e9
commit 66fe71e9cc
9 changed files with 333 additions and 36 deletions

View File

@@ -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]

View File

@@ -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-interceptor>>.
[[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 <<statemachine-examples-eventservice>> for detailed usage.

View File

@@ -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<S, E, T> implements StateMac
}
@Override
public final StateMachine<S, E> reset(StateMachine<S, E> stateMachine, T contextOjb) throws Exception {
public final StateMachine<S, E> restore(StateMachine<S, E> stateMachine, T contextOjb) throws Exception {
final StateMachineContext<S, E> context = stateMachinePersist.read(contextOjb);
stateMachine.stop();
stateMachine.getStateMachineAccessor().doWithAllRegions(new StateMachineFunction<StateMachineAccess<S, E>>() {

View File

@@ -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<S, E, T> {
* @return the state machine
* @throws Exception the exception in case or any persist error
*/
StateMachine<S, E> reset(StateMachine<S, E> stateMachine, T contextOjb) throws Exception;
StateMachine<S, E> restore(StateMachine<S, E> stateMachine, T contextOjb) throws Exception;
}

View File

@@ -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<String, String, String> persister = new DefaultStateMachinePersister<>(stateMachinePersist);
StateMachine<String, String> stateMachine1 = context.getBean("machine1", StateMachine.class);
StateMachine<String, String> 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<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial("S1")
.state("S1")
.state("S2");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withExternal()
.source("S1")
.target("S2")
.event("E1");
}
}
// end::snippetA[]
// tag::snippetB[]
static class InMemoryStateMachinePersist implements StateMachinePersist<String, String, String> {
private final HashMap<String, StateMachineContext<String, String>> contexts = new HashMap<>();
@Override
public void write(StateMachineContext<String, String> context, String contextOjb) throws Exception {
contexts.put(contextOjb, context);
}
@Override
public StateMachineContext<String, String> read(String contextOjb) throws Exception {
return contexts.get(contextOjb);
}
}
// end::snippetB[]
}

View File

@@ -56,7 +56,7 @@ public class StateMachinePersistTests extends AbstractStateMachineTests {
StateMachinePersister<String, String, String> 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<TestStates, TestEvents, String> 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<String, String, String> 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<String, String, String> 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<String, String, String> persister = new DefaultStateMachinePersister<>(stateMachinePersist);
StateMachine<String, String> stateMachine1 = context.getBean("machine1", StateMachine.class);
StateMachine<String, String> 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<String, String> {
@@ -328,6 +361,62 @@ public class StateMachinePersistTests extends AbstractStateMachineTests {
}
}
static class Config5 extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> 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<String, String> 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<String, String, String> {
private final HashMap<String, StateMachineContext<String, String>> contexts = new HashMap<>();

View File

@@ -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 <S> the type of state
* @param <E> the type of event
*/
public class RedisStateMachinePersister<S, E> extends AbstractStateMachinePersister<S, E, String> {
/**
* Instantiates a new redis state machine persister.
*
* @param stateMachinePersist the state machine persist
*/
public RedisStateMachinePersister(StateMachinePersist<S, E, String> stateMachinePersist) {
super(stateMachinePersist);
}
}

View File

@@ -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<States, Events>(connectionFactory);
return new RepositoryStateMachinePersist<States, Events>(repository);
}
@Bean
public RedisStateMachinePersister<States, Events> redisStateMachinePersister(
StateMachinePersist<States, Events, String> stateMachinePersist) {
return new RedisStateMachinePersister<States, Events>(stateMachinePersist);
}
//end::snippetD[]
@Bean

View File

@@ -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<States, Events> stateMachine;
@Autowired
private StateMachinePersist<States, Events, String> stateMachinePersist;
private StateMachinePersister<States, Events, String> 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<States, Events>(stateMachine.getState().getId(), null, null,
stateMachine.getExtendedState()), "testprefix:" + user);
stateMachinePersister.persist(stateMachine, "testprefix:" + user);
}
//end::snippetD[]
//tag::snippetE[]
private StateMachine<States, Events> resetStateMachineFromStore(String user) throws Exception {
final StateMachineContext<States, Events> context = stateMachinePersist.read("testprefix:" + user);
stateMachine.stop();
stateMachine.getStateMachineAccessor()
.doWithAllRegions(new StateMachineFunction<StateMachineAccess<States, Events>>() {
@Override
public void apply(StateMachineAccess<States, Events> function) {
function.resetStateMachine(context);
}
});
stateMachine.start();
return stateMachine;
return stateMachinePersister.restore(stateMachine, "testprefix:" + user);
}
//end::snippetE[]
}