Preliminary support for Spring Data persist

- Add new repository model for storing StateMachineContext
  via a new StateMachineRepository.
- New StateMachineRuntimePersister interface to abstract needed
  functionality to do a runtime machine persistence.
- As runtime persistence, as of now, is done via interceptors, define
  JpaRepositoryStateMachinePersist and JpaPersistingStateMachineInterceptor
  to define StateMachineRuntimePersister logic.
- Add new datajpapersist sample demonstrating new concepts.
- Keep tests related to jpa as there's not redis/mongo integration
  implemented in this first iteration.
- As this is going to be WIP until features around this issues
  are completed, docs, etc are not yet added. Also, interfaces and impls
  are subject to change during a process.
- Relates to #423
- Relates to #426
- Relates to #427
This commit is contained in:
Janne Valkealahti
2017-11-18 16:38:49 +00:00
parent d82bca1280
commit 33a5370d01
29 changed files with 1442 additions and 13 deletions

View File

@@ -79,6 +79,7 @@ import org.springframework.statemachine.state.StateHolder;
import org.springframework.statemachine.state.StateMachineState;
import org.springframework.statemachine.support.DefaultExtendedState;
import org.springframework.statemachine.support.LifecycleObjectSupport;
import org.springframework.statemachine.support.StateMachineInterceptor;
import org.springframework.statemachine.support.tree.Tree;
import org.springframework.statemachine.support.tree.Tree.Node;
import org.springframework.statemachine.support.tree.TreeTraverser;
@@ -319,6 +320,18 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
machine.addStateListener(listener);
}
List<StateMachineInterceptor<S,E>> interceptors = stateMachineModel.getConfigurationData().getStateMachineInterceptors();
if (interceptors != null) {
for (final StateMachineInterceptor<S, E> interceptor : interceptors) {
machine.getStateMachineAccessor().doWithRegion(new StateMachineFunction<StateMachineAccess<S,E>>() {
@Override
public void apply(StateMachineAccess<S, E> function) {
function.addStateMachineInterceptor(interceptor);
}
});
}
}
// go through holders and fix state references which
// were not known at a time holder was created
for (HolderListItem<S, E> holderItem : holderList) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2016 the original author or authors.
* Copyright 2015-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.
@@ -29,10 +29,12 @@ import org.springframework.statemachine.config.configurers.ConfigurationConfigur
import org.springframework.statemachine.config.configurers.DefaultConfigurationConfigurer;
import org.springframework.statemachine.config.configurers.DefaultDistributedStateMachineConfigurer;
import org.springframework.statemachine.config.configurers.DefaultMonitoringConfigurer;
import org.springframework.statemachine.config.configurers.DefaultPersistenceConfigurer;
import org.springframework.statemachine.config.configurers.DefaultSecurityConfigurer;
import org.springframework.statemachine.config.configurers.DefaultVerifierConfigurer;
import org.springframework.statemachine.config.configurers.DistributedStateMachineConfigurer;
import org.springframework.statemachine.config.configurers.MonitoringConfigurer;
import org.springframework.statemachine.config.configurers.PersistenceConfigurer;
import org.springframework.statemachine.config.configurers.SecurityConfigurer;
import org.springframework.statemachine.config.configurers.VerifierConfigurer;
import org.springframework.statemachine.config.model.ConfigurationData;
@@ -41,7 +43,9 @@ import org.springframework.statemachine.config.model.verifier.StateMachineModelV
import org.springframework.statemachine.ensemble.StateMachineEnsemble;
import org.springframework.statemachine.listener.StateMachineListener;
import org.springframework.statemachine.monitor.StateMachineMonitor;
import org.springframework.statemachine.persist.StateMachineRuntimePersister;
import org.springframework.statemachine.security.SecurityRule;
import org.springframework.statemachine.support.StateMachineInterceptor;
/**
* {@link AnnotationBuilder} for {@link StatesData}.
@@ -70,6 +74,8 @@ public class StateMachineConfigurationBuilder<S, E>
private SecurityRule eventSecurityRule;
private SecurityRule transitionSecurityRule;
private StateMachineMonitor<S, E> stateMachineMonitor;
private final List<StateMachineInterceptor<S, E>> interceptors = new ArrayList<StateMachineInterceptor<S, E>>();
private StateMachineRuntimePersister<S, E> persister;
/**
* Instantiates a new state machine configuration builder.
@@ -123,11 +129,23 @@ public class StateMachineConfigurationBuilder<S, E>
return apply(new DefaultMonitoringConfigurer<S, E>());
}
@Override
public PersistenceConfigurer<S, E> withPersistence() throws Exception {
return apply(new DefaultPersistenceConfigurer<S, E>());
}
@Override
protected ConfigurationData<S, E> performBuild() throws Exception {
ArrayList<StateMachineInterceptor<S, E>> interceptorsCopy = new ArrayList<StateMachineInterceptor<S, E>>(interceptors);
if (persister != null) {
StateMachineInterceptor<S, E> interceptor = persister.getInterceptor();
if (interceptor != null) {
interceptorsCopy.add((StateMachineInterceptor<S, E>) interceptor);
}
}
return new ConfigurationData<S, E>(beanFactory, taskExecutor, taskScheculer, autoStart, ensemble, listeners,
securityEnabled, transitionSecurityAccessDecisionManager, eventSecurityAccessDecisionManager, eventSecurityRule,
transitionSecurityRule, verifierEnabled, verifier, machineId, stateMachineMonitor);
transitionSecurityRule, verifierEnabled, verifier, machineId, stateMachineMonitor, interceptorsCopy);
}
/**
@@ -265,4 +283,13 @@ public class StateMachineConfigurationBuilder<S, E>
public void setVerifier(StateMachineModelVerifier<S, E> verifier) {
this.verifier = verifier;
}
/**
* Sets the state machine runtime persister.
*
* @param persister the state machine runtime persister
*/
public void setStateMachineRuntimePersister(StateMachineRuntimePersister<S, E> persister) {
this.persister = persister;
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.statemachine.config.builders;
import org.springframework.statemachine.config.configurers.ConfigurationConfigurer;
import org.springframework.statemachine.config.configurers.DistributedStateMachineConfigurer;
import org.springframework.statemachine.config.configurers.MonitoringConfigurer;
import org.springframework.statemachine.config.configurers.PersistenceConfigurer;
import org.springframework.statemachine.config.configurers.SecurityConfigurer;
import org.springframework.statemachine.config.configurers.VerifierConfigurer;
@@ -70,4 +71,12 @@ public interface StateMachineConfigurationConfigurer<S, E> {
* @throws Exception if configuration error happens
*/
MonitoringConfigurer<S, E> withMonitoring() throws Exception;
/**
* Gets a configurer for state machine persistence.
*
* @return {@link PersistenceConfigurer} for chaining
* @throws Exception if configuration error happens
*/
PersistenceConfigurer<S, E> withPersistence() throws Exception;
}

View File

@@ -0,0 +1,48 @@
/*
* 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.config.configurers;
import org.springframework.statemachine.config.builders.StateMachineConfigurationBuilder;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerAdapter;
import org.springframework.statemachine.config.model.ConfigurationData;
import org.springframework.statemachine.persist.StateMachineRuntimePersister;
/**
* Default implementation of a {@link PersistenceConfigurer}.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public class DefaultPersistenceConfigurer<S, E> extends
AnnotationConfigurerAdapter<ConfigurationData<S, E>, StateMachineConfigurationConfigurer<S, E>, StateMachineConfigurationBuilder<S, E>>
implements PersistenceConfigurer<S, E> {
private StateMachineRuntimePersister<S, E> persister;
@Override
public void configure(StateMachineConfigurationBuilder<S, E> builder) throws Exception {
builder.setStateMachineRuntimePersister(persister);
}
@Override
public PersistenceConfigurer<S, E> runtimePersister(StateMachineRuntimePersister<S, E> persister) {
this.persister = persister;
return this;
}
}

View File

@@ -0,0 +1,40 @@
/*
* 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.config.configurers;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.common.annotation.AnnotationConfigurerBuilder;
import org.springframework.statemachine.persist.StateMachineRuntimePersister;
/**
* Base {@code PersistenceConfigurer} interface for configuring state machine persistence.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public interface PersistenceConfigurer<S, E> extends
AnnotationConfigurerBuilder<StateMachineConfigurationConfigurer<S, E>> {
/**
* Specify a state machine runtime persister.
*
* @param persister the state machine runtime persister
* @return configurer for chaining
*/
PersistenceConfigurer<S, E> runtimePersister(StateMachineRuntimePersister<S, E> persister);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2016 the original author or authors.
* 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.
@@ -31,6 +31,7 @@ import org.springframework.statemachine.ensemble.StateMachineEnsemble;
import org.springframework.statemachine.listener.StateMachineListener;
import org.springframework.statemachine.monitor.StateMachineMonitor;
import org.springframework.statemachine.security.SecurityRule;
import org.springframework.statemachine.support.StateMachineInterceptor;
/**
* Configuration object used to keep things together in {@link StateMachineConfigurationBuilder}.
@@ -57,13 +58,14 @@ public class ConfigurationData<S, E> {
private final SecurityRule eventSecurityRule;
private final SecurityRule transitionSecurityRule;
private final StateMachineMonitor<S, E> stateMachineMonitor;
private final List<StateMachineInterceptor<S, E>> interceptors;
/**
* Instantiates a new state machine configuration config data.
*/
public ConfigurationData() {
this(null, new SyncTaskExecutor(), new ConcurrentTaskScheduler(), false, null, new ArrayList<StateMachineListener<S, E>>(), false,
null, null, null, null, true, new DefaultStateMachineModelVerifier<S, E>(), null, null);
null, null, null, null, true, new DefaultStateMachineModelVerifier<S, E>(), null, null, null);
}
/**
@@ -84,13 +86,15 @@ public class ConfigurationData<S, E> {
* @param verifier the state machine model verifier
* @param machineId the machine id
* @param stateMachineMonitor the state machine monitor
* @param interceptors the state machine interceptors.
*/
public ConfigurationData(BeanFactory beanFactory, TaskExecutor taskExecutor,
TaskScheduler taskScheduler, boolean autoStart, StateMachineEnsemble<S, E> ensemble,
List<StateMachineListener<S, E>> listeners, boolean securityEnabled,
AccessDecisionManager transitionSecurityAccessDecisionManager, AccessDecisionManager eventSecurityAccessDecisionManager,
SecurityRule eventSecurityRule, SecurityRule transitionSecurityRule, boolean verifierEnabled,
StateMachineModelVerifier<S, E> verifier, String machineId, StateMachineMonitor<S, E> stateMachineMonitor) {
StateMachineModelVerifier<S, E> verifier, String machineId, StateMachineMonitor<S, E> stateMachineMonitor,
List<StateMachineInterceptor<S, E>> interceptors) {
this.beanFactory = beanFactory;
this.taskExecutor = taskExecutor;
this.taskScheduler = taskScheduler;
@@ -106,6 +110,7 @@ public class ConfigurationData<S, E> {
this.verifier = verifier;
this.machineId = machineId;
this.stateMachineMonitor = stateMachineMonitor;
this.interceptors = interceptors;
}
public String getMachineId() {
@@ -237,4 +242,13 @@ public class ConfigurationData<S, E> {
public SecurityRule getTransitionSecurityRule() {
return transitionSecurityRule;
}
/**
* Gets the state machine interceptors.
*
* @return the state machine interceptors
*/
public List<StateMachineInterceptor<S, E>> getStateMachineInterceptors() {
return interceptors;
}
}

View File

@@ -0,0 +1,153 @@
/*
* 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.persist;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.springframework.messaging.Message;
import org.springframework.statemachine.ExtendedState;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.StateMachineContext;
import org.springframework.statemachine.StateMachineException;
import org.springframework.statemachine.StateMachinePersist;
import org.springframework.statemachine.region.Region;
import org.springframework.statemachine.state.AbstractState;
import org.springframework.statemachine.state.HistoryPseudoState;
import org.springframework.statemachine.state.PseudoState;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.support.AbstractStateMachine;
import org.springframework.statemachine.support.DefaultExtendedState;
import org.springframework.statemachine.support.DefaultStateMachineContext;
import org.springframework.statemachine.support.StateMachineInterceptor;
import org.springframework.statemachine.support.StateMachineInterceptorAdapter;
import org.springframework.statemachine.transition.Transition;
import org.springframework.statemachine.transition.TransitionKind;
/**
* Base class for {@link StateMachineInterceptor} persisting {@link StateMachineContext}s.
* This class is to be used as a base implementation which wants to persist a machine which
* is about to kept running as normal use case for persistence is to stop machine, persist and
* then start it again.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public abstract class AbstractPersistingStateMachineInterceptor<S, E> extends StateMachineInterceptorAdapter<S, E>
implements StateMachinePersist<S, E, Object> {
@Override
public void preStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition, StateMachine<S, E> stateMachine) {
// try to persist context and in case of failure, interceptor
// call chain aborts transition
// TODO: should probably come up with a policy vs. not force feeding this functionality
try {
write(buildStateMachineContext(stateMachine, state), null);
} catch (Exception e) {
throw new StateMachineException("Unable to persist stateMachineContext", e);
}
}
@Override
public void postStateChange(State<S, E> state, Message<E> message, Transition<S, E> transition, StateMachine<S, E> stateMachine) {
// initial transitions are never intercepted as those cannot fail or get aborted.
// for now, handle persistence in post state change
// TODO: consider intercept initial transition, but not aborting if error is thrown?
if (state != null && transition != null && transition.getKind() == TransitionKind.INITIAL) {
try {
write(buildStateMachineContext(stateMachine, state), null);
} catch (Exception e) {
throw new StateMachineException("Unable to persist stateMachineContext", e);
}
}
}
/**
* Write {@link StateMachineContext} into persistent store.
*
* @param context the state machine context
* @param contextObj the context object
*/
public abstract void write(StateMachineContext<S, E> context, Object contextObj) throws Exception;
/**
* Read {@link StateMachineContext} from persistent store.
*
* @param contextObj the context object
* @return the state machine context
*/
public abstract StateMachineContext<S, E> read(Object contextObj) throws Exception;
/**
* Builds the state machine context.
*
* @param stateMachine the state machine
* @param state the state
* @return the state machine context
*/
protected StateMachineContext<S, E> buildStateMachineContext(StateMachine<S, E> stateMachine, State<S, E> state) {
ExtendedState extendedState = new DefaultExtendedState();
extendedState.getVariables().putAll(stateMachine.getExtendedState().getVariables());
ArrayList<StateMachineContext<S, E>> childs = new ArrayList<StateMachineContext<S, E>>();
S id = null;
if (state.isSubmachineState()) {
id = getDeepState(state);
} else if (state.isOrthogonal()) {
Collection<Region<S, E>> regions = ((AbstractState<S, E>)state).getRegions();
for (Region<S, E> r : regions) {
StateMachine<S, E> rsm = (StateMachine<S, E>) r;
childs.add(buildStateMachineContext(rsm, state));
}
id = state.getId();
} else {
id = state.getId();
}
// building history state mappings
Map<S, S> historyStates = new HashMap<S, S>();
PseudoState<S, E> historyState = ((AbstractStateMachine<S, E>) stateMachine).getHistoryState();
if (historyState != null) {
historyStates.put(null, ((HistoryPseudoState<S, E>)historyState).getState().getId());
}
Collection<State<S, E>> states = stateMachine.getStates();
for (State<S, E> ss : states) {
if (ss.isSubmachineState()) {
StateMachine<S, E> submachine = ((AbstractState<S, E>) ss).getSubmachine();
PseudoState<S, E> ps = ((AbstractStateMachine<S, E>) submachine).getHistoryState();
if (ps != null) {
State<S, E> pss = ((HistoryPseudoState<S, E>)ps).getState();
if (pss != null) {
historyStates.put(ss.getId(), pss.getId());
}
}
}
}
return new DefaultStateMachineContext<S, E>(childs, id, null, null, extendedState, historyStates, stateMachine.getId());
}
private S getDeepState(State<S, E> state) {
Collection<S> ids1 = state.getIds();
@SuppressWarnings("unchecked")
S[] ids2 = (S[]) ids1.toArray();
// TODO: can this be empty as then we'd get error?
return ids2[ids2.length-1];
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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.persist;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.support.StateMachineInterceptor;
/**
* Interface defining a runtime persistence of a {@link StateMachine}.
*
* @author Janne Valkealahti
*
* @param <S> the type of state
* @param <E> the type of event
*/
public interface StateMachineRuntimePersister<S, E> {
/**
* Gets a {@link StateMachineInterceptor} handling machine persistence.
*
* @return the interceptor handling persistence
*/
StateMachineInterceptor<S, E> getInterceptor();
}

View File

@@ -56,7 +56,7 @@ public class StateMachineModelTests {
ConfigurationData<String, String> configurationData = new ConfigurationData<>(beanFactory, taskExecutor, taskScheduler, autoStart,
ensemble, listeners, securityEnabled, transitionSecurityAccessDecisionManager, eventSecurityAccessDecisionManager,
eventSecurityRule, transitionSecurityRule, verifierEnabled, verifier, null, null);
eventSecurityRule, transitionSecurityRule, verifierEnabled, verifier, null, null, null);
Collection<StateData<String, String>> stateData = new ArrayList<>();
StateData<String, String> stateData1 = new StateData<String, String>(null, null, "S1", null, null, null);