Refactor region handling and persistence

- Make kryo in AbstractKryoStateMachineSerialisationService aware of same classloader
  most likely use in an app. This takes away some of those weird kryo
  errors you see with a web apps.
- Add context references concept to StateMachineContext which can be used
  to store reference id and then individual running machines with regions
  can independently store their states. Whole machine state can then get
  restored more accurately.
- Add new `region(String id)` to StateConfigurer which can be used to set region id.
  This is equivalent as setting region id with json based machine structure where
  you need to define region id's with orthogonal regions are in use.
- Add new datajpamultipersist sample showing running regions and how those are
  persisted to a database.
- Fixes #617
- Fixes #605
- Fixes #615
This commit is contained in:
Janne Valkealahti
2019-01-12 08:36:51 +00:00
parent c700301767
commit 84ca0aec3e
34 changed files with 1297 additions and 51 deletions

View File

@@ -128,6 +128,19 @@ project('spring-statemachine-samples-datajpa') {
}
}
project('spring-statemachine-samples-datajpamultipersist') {
description = 'Spring State Machine Data Jpa Multi Persist Sample'
dependencies {
compile project(":spring-statemachine-autoconfigure")
compile project(":spring-statemachine-data-common:spring-statemachine-data-jpa")
compile("org.springframework.boot:spring-boot-starter-web")
compile("org.springframework.boot:spring-boot-starter-thymeleaf")
compile("org.springframework.boot:spring-boot-starter-data-jpa")
compile("org.springframework.boot:spring-boot-devtools")
compile("com.h2database:h2")
}
}
project('spring-statemachine-samples-datapersist') {
description = 'Spring State Machine Data Persist Sample'
dependencies {

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2018 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 demo.datajpamultipersist;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
//tag::snippetA[]
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
//end::snippetA[]

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2018 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 demo.datajpamultipersist;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.statemachine.config.EnableStateMachineFactory;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.StateMachineFactory;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.builders.StateMachineModelConfigurer;
import org.springframework.statemachine.config.model.StateMachineModelFactory;
import org.springframework.statemachine.data.RepositoryState;
import org.springframework.statemachine.data.RepositoryStateMachineModelFactory;
import org.springframework.statemachine.data.RepositoryTransition;
import org.springframework.statemachine.data.StateRepository;
import org.springframework.statemachine.data.TransitionRepository;
import org.springframework.statemachine.data.jpa.JpaPersistingStateMachineInterceptor;
import org.springframework.statemachine.data.jpa.JpaStateMachineRepository;
import org.springframework.statemachine.data.support.StateMachineJackson2RepositoryPopulatorFactoryBean;
import org.springframework.statemachine.persist.StateMachineRuntimePersister;
import org.springframework.statemachine.service.DefaultStateMachineService;
import org.springframework.statemachine.service.StateMachineService;
@Configuration
public class StateMachineConfig {
@Bean
public StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister(
JpaStateMachineRepository jpaStateMachineRepository) {
return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository);
}
@Bean
public StateMachineService<String, String> stateMachineService(
StateMachineFactory<String, String> stateMachineFactory,
StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister) {
return new DefaultStateMachineService<String, String>(stateMachineFactory, stateMachineRuntimePersister);
}
@Bean
public StateMachineJackson2RepositoryPopulatorFactoryBean jackson2RepositoryPopulatorFactoryBean() {
StateMachineJackson2RepositoryPopulatorFactoryBean factoryBean = new StateMachineJackson2RepositoryPopulatorFactoryBean();
factoryBean.setResources(new Resource[] { new ClassPathResource("data.json") });
return factoryBean;
}
@Configuration
@EnableStateMachineFactory
public static class Config extends StateMachineConfigurerAdapter<String, String> {
@Autowired
private StateRepository<? extends RepositoryState> stateRepository;
@Autowired
private TransitionRepository<? extends RepositoryTransition> transitionRepository;
@Autowired
private StateMachineRuntimePersister<String, String, String> stateMachineRuntimePersister;
@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config)
throws Exception {
config
.withPersistence()
.runtimePersister(stateMachineRuntimePersister);
}
@Override
public void configure(StateMachineModelConfigurer<String, String> model)
throws Exception {
model
.withModel()
.factory(modelFactory());
}
@Bean
public StateMachineModelFactory<String, String> modelFactory() {
return new RepositoryStateMachineModelFactory(stateRepository, transitionRepository);
}
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright 2019 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 demo.datajpamultipersist;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.StateMachineContext;
import org.springframework.statemachine.StateMachinePersist;
import org.springframework.statemachine.data.RepositoryTransition;
import org.springframework.statemachine.data.TransitionRepository;
import org.springframework.statemachine.service.StateMachineService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class StateMachineController {
public final static String MACHINE_ID_1 = "datajpamultipersist1";
public final static String MACHINE_ID_2 = "datajpamultipersist2";
public final static String MACHINE_ID_2R1 = "datajpamultipersist2#R1";
public final static String MACHINE_ID_2R2 = "datajpamultipersist2#R2";
private final static String[] MACHINES = new String[] { MACHINE_ID_1, MACHINE_ID_2 };
private final StateMachineLogListener listener = new StateMachineLogListener();
@Autowired
private StateMachineService<String, String> stateMachineService;
@Autowired
private StateMachinePersist<String, String, String> stateMachinePersist;
@Autowired
private TransitionRepository<? extends RepositoryTransition> transitionRepository;
private StateMachine<String, String> currentStateMachine;
@RequestMapping("/")
public String home() {
return "redirect:/state";
}
@RequestMapping("/state")
public String feedAndGetStates(
@RequestParam(value = "events", required = false) List<String> events,
@RequestParam(value = "machine", required = false, defaultValue = MACHINE_ID_1) String machine,
Model model) throws Exception {
StateMachine<String, String> stateMachine = getStateMachine(machine);
if (events != null) {
for (String event : events) {
stateMachine.sendEvent(event);
}
}
StringBuilder contextBuf = new StringBuilder();
StateMachineContext<String, String> stateMachineContext = stateMachinePersist.read(machine);
if (stateMachineContext != null) {
contextBuf.append(stateMachineContext.toString());
}
if (ObjectUtils.nullSafeEquals(machine, MACHINE_ID_2)) {
stateMachineContext = stateMachinePersist.read(MACHINE_ID_2R1);
if (stateMachineContext != null) {
contextBuf.append("\n---\n");
contextBuf.append(stateMachineContext.toString());
}
stateMachineContext = stateMachinePersist.read(MACHINE_ID_2R2);
if (stateMachineContext != null) {
contextBuf.append("\n---\n");
contextBuf.append(stateMachineContext.toString());
}
}
model.addAttribute("allMachines", MACHINES);
model.addAttribute("machine", machine);
model.addAttribute("currentMachine", currentStateMachine);
model.addAttribute("allEvents", getEvents());
model.addAttribute("messages", createMessages(listener.getMessages()));
model.addAttribute("context", contextBuf.toString());
return "states";
}
private synchronized StateMachine<String, String> getStateMachine(String machineId) throws Exception {
listener.resetMessages();
if (currentStateMachine == null) {
currentStateMachine = stateMachineService.acquireStateMachine(machineId, false);
currentStateMachine.addStateListener(listener);
currentStateMachine.start();
} else if (!ObjectUtils.nullSafeEquals(currentStateMachine.getId(), machineId)) {
stateMachineService.releaseStateMachine(currentStateMachine.getId());
currentStateMachine.stop();
currentStateMachine = stateMachineService.acquireStateMachine(machineId, false);
currentStateMachine.addStateListener(listener);
currentStateMachine.start();
}
return currentStateMachine;
}
private String[] getEvents() {
List<String> events = new ArrayList<>();
for (RepositoryTransition t : transitionRepository.findAll()) {
events.add(t.getEvent());
}
return events.toArray(new String[0]);
}
private String createMessages(List<String> messages) {
StringBuilder buf = new StringBuilder();
for (String message : messages) {
buf.append(message);
buf.append("\n");
}
return buf.toString();
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2018 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 demo.datajpamultipersist;
import java.util.LinkedList;
import java.util.List;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateContext.Stage;
import org.springframework.statemachine.listener.StateMachineListenerAdapter;
public class StateMachineLogListener extends StateMachineListenerAdapter<String, String> {
private final LinkedList<String> messages = new LinkedList<String>();
public List<String> getMessages() {
return messages;
}
public void resetMessages() {
messages.clear();
}
@Override
public void stateContext(StateContext<String, String> stateContext) {
if (stateContext.getStage() == Stage.STATE_ENTRY) {
messages.addFirst("Enter " + stateContext.getTarget().getId());
} else if (stateContext.getStage() == Stage.STATE_EXIT) {
messages.addFirst("Exit " + stateContext.getSource().getId());
} else if (stateContext.getStage() == Stage.STATEMACHINE_START) {
messages.addLast("Machine started");
} else if (stateContext.getStage() == Stage.STATEMACHINE_STOP) {
messages.addFirst("Machine stopped");
}
}
}

View File

@@ -0,0 +1,7 @@
logging:
level:
root: INFO
org.springframework.statemachine: DEBUG
security:
basic:
enabled: false

View File

@@ -0,0 +1,172 @@
[
{
"@id": "100",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction",
"spel": "T(System).out.println('hello exit S1')"
},
{
"@id": "101",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction",
"spel": "T(System).out.println('hello entry S2')"
},
{
"@id": "102",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction",
"spel": "T(System).out.println('hello state S3')"
},
{
"@id": "103",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryAction",
"spel": "T(System).out.println('hello')"
},
{
"@id": "10",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
"machineId": "datajpamultipersist1",
"initial": true,
"state": "S1",
"exitActions": ["100"]
},
{
"@id": "11",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
"machineId": "datajpamultipersist1",
"initial": false,
"state": "S2",
"entryActions": ["101"]
},
{
"@id": "12",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
"machineId": "datajpamultipersist1",
"initial": false,
"state": "S3",
"stateActions": ["102"]
},
{
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
"machineId": "datajpamultipersist1",
"source": "10",
"target": "11",
"event": "E1",
"kind": "EXTERNAL"
},
{
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
"machineId": "datajpamultipersist1",
"source": "11",
"target": "12",
"event": "E2",
"actions": ["103"]
},
{
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
"machineId": "datajpamultipersist1",
"source": "12",
"target": "11",
"event": "E3",
"actions": ["103"]
},
{
"@id": "20",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
"machineId": "datajpamultipersist2",
"region": "R1",
"initial": true,
"state": "S10",
"exitActions": ["100"]
},
{
"@id": "21",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
"machineId": "datajpamultipersist2",
"region": "R1",
"initial": false,
"state": "S11",
"entryActions": ["101"]
},
{
"@id": "22",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
"machineId": "datajpamultipersist2",
"region": "R1",
"initial": false,
"state": "S12",
"stateActions": ["102"]
},
{
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
"machineId": "datajpamultipersist2",
"source": "20",
"target": "21",
"event": "E10",
"kind": "EXTERNAL"
},
{
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
"machineId": "datajpamultipersist2",
"source": "21",
"target": "22",
"event": "E11",
"actions": ["103"]
},
{
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
"machineId": "datajpamultipersist2",
"source": "22",
"target": "21",
"event": "E12",
"actions": ["103"]
},
{
"@id": "30",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
"machineId": "datajpamultipersist2",
"region": "R2",
"initial": true,
"state": "S20",
"exitActions": ["100"]
},
{
"@id": "31",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
"machineId": "datajpamultipersist2",
"region": "R2",
"initial": false,
"state": "S21",
"entryActions": ["101"]
},
{
"@id": "32",
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryState",
"machineId": "datajpamultipersist2",
"region": "R2",
"initial": false,
"state": "S22",
"stateActions": ["102"]
},
{
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
"machineId": "datajpamultipersist2",
"source": "30",
"target": "31",
"event": "E20",
"kind": "EXTERNAL"
},
{
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
"machineId": "datajpamultipersist2",
"source": "31",
"target": "32",
"event": "E21",
"actions": ["103"]
},
{
"_class": "org.springframework.statemachine.data.jpa.JpaRepositoryTransition",
"machineId": "datajpamultipersist2",
"source": "32",
"target": "31",
"event": "E22",
"actions": ["103"]
}
]

View File

@@ -0,0 +1,52 @@
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring Statemachine Demo</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div>
<a href="/h2-console" target="_blank">h2 console</a>
</div>
<form action="#" data-th-action="@{/state}" data-th-object="${model}" method="post">
<div>
<p th:text="'Choose machine'"/>
<ul>
<li th:each="ty : ${allMachines}">
<input type="radio" name="machine" th:value="${ty}" th:checked="${ty} == ${machine}"/>
<label th:text="${ty}">replaced</label>
</li>
</ul>
</div>
<div>
<p th:text="'Choose events'"/>
<ul>
<li th:each="ty : ${allEvents}">
<input type="checkbox" name="events" th:value="${ty}" />
<label th:text="${ty}">replaced</label>
</li>
</ul>
</div>
<button type="submit">Send Events</button>
</form>
<div>
<h3>Events</h3>
</div>
<div>
<textarea th:text="${messages}" rows="15" cols="100"/>
</div>
<div>
<h3>StateMachineContext</h3>
</div>
<div>
<textarea th:text="${context}" rows="15" cols="100"/>
</div>
<div>
<h3>Current Machine</h3>
</div>
<div>
<textarea th:text="${currentMachine}" rows="10" cols="100"/>
</div>
</body>
</html>

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2019 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 demo.datajpamultipersist;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import demo.datajpamultipersist.Application;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = { Application.class })
@WebAppConfiguration
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
public class DataJpaMultiPersistTests {
private MockMvc mvc;
@Autowired
private WebApplicationContext context;
@Test
public void testHome() throws Exception {
mvc.
perform(get("/state")).
andExpect(status().isOk()).
andExpect(content().string(allOf(
containsString("Enter S1"),
containsString("Machine started"))));
}
@Test
public void testSendEventE1WithMachine1() throws Exception {
mvc.
perform(get("/state")
.param("events", "E1")
.param("machine", StateMachineController.MACHINE_ID_1)).
andExpect(status().isOk()).
andExpect(content().string(allOf(
containsString("Enter S1"),
containsString("Exit S1"),
containsString("Enter S2"))));
}
@Test
public void testSendEventsE1E2WithMachine1() throws Exception {
mvc.
perform(get("/state")
.param("events", "E1")
.param("events", "E2")
.param("machine", StateMachineController.MACHINE_ID_1)).
andExpect(status().isOk()).
andExpect(content().string(allOf(
containsString("Enter S1"),
containsString("Exit S1"),
containsString("Enter S2"),
containsString("Exit S2"),
containsString("Enter S3"))));
}
@Test
public void testWithMachine2() throws Exception {
mvc.
perform(get("/state")
.param("machine", StateMachineController.MACHINE_ID_2)).
andExpect(status().isOk()).
andExpect(content().string(allOf(
containsString("Enter S10"),
containsString("Enter S20"),
containsString("Enter null"))));
}
@Test
public void testSendEventsE10E20WithMachine2() throws Exception {
mvc.
perform(get("/state")
.param("events", "E10")
.param("events", "E20")
.param("machine", StateMachineController.MACHINE_ID_2)).
andExpect(status().isOk()).
andExpect(content().string(allOf(
containsString("Enter S10"),
containsString("Enter S20"),
containsString("Enter S11"),
containsString("Enter S21"),
containsString("Enter null"))));
}
@Before
public void setup() throws Exception {
mvc = MockMvcBuilders.webAppContextSetup(context).build();
}
}