Add base feature for error handling
- Add related error handling methods to StateMachine. - Enhance StateMachineListener and context events around error events. - Modify StateMachineInterceptor and stuff around it order to handle errors. - Fixes #6, fixes #77
This commit is contained in:
@@ -51,4 +51,18 @@ public interface StateMachine<S, E> extends Region<S, E> {
|
||||
*/
|
||||
StateMachineAccessor<S, E> getStateMachineAccessor();
|
||||
|
||||
/**
|
||||
* Sets the state machine error.
|
||||
*
|
||||
* @param exception the new state machine error
|
||||
*/
|
||||
void setStateMachineError(Exception exception);
|
||||
|
||||
/**
|
||||
* Checks for state machine error.
|
||||
*
|
||||
* @return true, if error has been set
|
||||
*/
|
||||
boolean hasStateMachineError();
|
||||
|
||||
}
|
||||
|
||||
@@ -133,6 +133,16 @@ public class DistributedStateMachine<S, E> extends LifecycleObjectSupport implem
|
||||
return delegate.isComplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStateMachineError(Exception exception) {
|
||||
delegate.setStateMachineError(exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStateMachineError() {
|
||||
return delegate.hasStateMachineError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addStateListener(StateMachineListener<S, E> listener) {
|
||||
delegate.addStateListener(listener);
|
||||
@@ -215,6 +225,11 @@ public class DistributedStateMachine<S, E> extends LifecycleObjectSupport implem
|
||||
return stateContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Exception stateMachineError(StateMachine<S, E> stateMachine, Exception exception) {
|
||||
return exception;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -269,9 +284,8 @@ public class DistributedStateMachine<S, E> extends LifecycleObjectSupport implem
|
||||
|
||||
@Override
|
||||
public void ensembleError(StateMachineEnsembleException exception) {
|
||||
// TODO: when we get support for sm error handling,
|
||||
// propagate this exception there
|
||||
log.error("Ensemble error", exception);
|
||||
setStateMachineError(exception);
|
||||
throw exception;
|
||||
}
|
||||
|
||||
|
||||
@@ -115,4 +115,11 @@ public class DefaultStateMachineEventPublisher implements StateMachineEventPubli
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishStateMachineError(Object source, StateMachine<?, ?> stateMachine, Exception exception) {
|
||||
if (applicationEventPublisher != null) {
|
||||
applicationEventPublisher.publishEvent(new OnStateMachineError(source, stateMachine, exception));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2015 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.event;
|
||||
|
||||
import org.springframework.statemachine.StateMachine;
|
||||
|
||||
/**
|
||||
* Generic event representing that state machine has been entered in
|
||||
* error it cannot recover.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class OnStateMachineError extends StateMachineEvent {
|
||||
|
||||
private final StateMachine<?, ?> stateMachine;
|
||||
private final Exception exception;
|
||||
|
||||
/**
|
||||
* Instantiates a new on state exit event.
|
||||
*
|
||||
* @param source the source
|
||||
* @param stateMachine the statemachine
|
||||
* @param exception the exception
|
||||
*/
|
||||
public OnStateMachineError(Object source, StateMachine<?, ?> stateMachine, Exception exception) {
|
||||
super(source);
|
||||
this.stateMachine = stateMachine;
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the statemachine.
|
||||
*
|
||||
* @return the statemachine
|
||||
*/
|
||||
public StateMachine<?, ?> getStateMachine() {
|
||||
return stateMachine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the exception.
|
||||
*
|
||||
* @return the exception
|
||||
*/
|
||||
public Exception getException() {
|
||||
return exception;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "OnStateMachineError [stateMachine=" + stateMachine + ", exception=" + exception + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -101,4 +101,13 @@ public interface StateMachineEventPublisher {
|
||||
*/
|
||||
void publishStateMachineStop(Object source, StateMachine<?, ?> stateMachine);
|
||||
|
||||
/**
|
||||
* Publish a state machine error.
|
||||
*
|
||||
* @param source the source
|
||||
* @param stateMachine the state machine
|
||||
* @param exception the exception
|
||||
*/
|
||||
void publishStateMachineError(Object source, StateMachine<?, ?> stateMachine, Exception exception);
|
||||
|
||||
}
|
||||
|
||||
@@ -105,4 +105,12 @@ public class CompositeStateMachineListener<S,E> extends AbstractCompositeListene
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stateMachineError(StateMachine<S, E> stateMachine, Exception exception) {
|
||||
for (Iterator<StateMachineListener<S, E>> iterator = getListeners().reverse(); iterator.hasNext();) {
|
||||
StateMachineListener<S, E> listener = iterator.next();
|
||||
listener.stateMachineError(stateMachine, exception);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -94,4 +94,12 @@ public interface StateMachineListener<S,E> {
|
||||
*/
|
||||
void stateMachineStopped(StateMachine<S, E> stateMachine);
|
||||
|
||||
/**
|
||||
* Notified when statemachine enters error it can't recover from.
|
||||
*
|
||||
* @param stateMachine the state machine
|
||||
* @param exception the exception
|
||||
*/
|
||||
void stateMachineError(StateMachine<S, E> stateMachine, Exception exception);
|
||||
|
||||
}
|
||||
|
||||
@@ -67,4 +67,8 @@ public class StateMachineListenerAdapter<S, E> implements StateMachineListener<S
|
||||
public void stateMachineStopped(StateMachine<S, E> stateMachine) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stateMachineError(StateMachine<S, E> stateMachine, Exception exception) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -92,6 +92,8 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
|
||||
|
||||
private volatile State<S,E> currentState;
|
||||
|
||||
private volatile Exception currentError;
|
||||
|
||||
private volatile PseudoState<S, E> history;
|
||||
|
||||
private final Map<String, StateMachineOnTransitionHandler<S, E>> handlers = new HashMap<String, StateMachineOnTransitionHandler<S,E>>();
|
||||
@@ -180,6 +182,11 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
|
||||
|
||||
@Override
|
||||
public boolean sendEvent(Message<E> event) {
|
||||
if (hasStateMachineError()) {
|
||||
// TODO: should we throw exception?
|
||||
notifyEventNotAccepted(event);
|
||||
return false;
|
||||
}
|
||||
if (isComplete() || !isRunning()) {
|
||||
notifyEventNotAccepted(event);
|
||||
return false;
|
||||
@@ -292,6 +299,24 @@ public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSuppo
|
||||
initialEnabled = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStateMachineError(Exception exception) {
|
||||
if (exception == null) {
|
||||
currentError = null;
|
||||
} else {
|
||||
exception = getStateMachineInterceptors().stateMachineError(this, exception);
|
||||
currentError = exception;
|
||||
}
|
||||
if (currentError != null) {
|
||||
notifyStateMachineError(this, currentError);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStateMachineError() {
|
||||
return currentError != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addStateListener(StateMachineListener<S, E> listener) {
|
||||
getStateListener().register(listener);
|
||||
|
||||
@@ -73,4 +73,13 @@ public interface StateMachineInterceptor<S, E> {
|
||||
*/
|
||||
StateContext<S, E> postTransition(StateContext<S, E> stateContext);
|
||||
|
||||
/**
|
||||
* State when state machine is about to enter error it can't recover.
|
||||
*
|
||||
* @param stateMachine the state machine
|
||||
* @param exception the exception
|
||||
* @return the exception
|
||||
*/
|
||||
Exception stateMachineError(StateMachine<S, E> stateMachine, Exception exception);
|
||||
|
||||
}
|
||||
|
||||
@@ -51,4 +51,9 @@ public class StateMachineInterceptorAdapter<S, E> implements StateMachineInterce
|
||||
return stateContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Exception stateMachineError(StateMachine<S, E> stateMachine, Exception exception) {
|
||||
return exception;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -131,4 +131,20 @@ public class StateMachineInterceptorList<S, E> {
|
||||
return stateContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* State machine error.
|
||||
*
|
||||
* @param stateMachine the state machine
|
||||
* @param exception the exception
|
||||
* @return the exception
|
||||
*/
|
||||
public Exception stateMachineError(StateMachine<S, E> stateMachine, Exception exception) {
|
||||
for (StateMachineInterceptor<S, E> interceptor : interceptors) {
|
||||
if ((exception = interceptor.stateMachineError(stateMachine, exception)) == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return exception;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -183,6 +183,16 @@ public abstract class StateMachineObjectSupport<S, E> extends LifecycleObjectSup
|
||||
}
|
||||
}
|
||||
|
||||
protected void notifyStateMachineError(StateMachine<S, E> stateMachine, Exception exception) {
|
||||
stateListener.stateMachineError(stateMachine, exception);
|
||||
if (contextEventsEnabled) {
|
||||
StateMachineEventPublisher eventPublisher = getStateMachineEventPublisher();
|
||||
if (eventPublisher != null) {
|
||||
eventPublisher.publishStateMachineError(this, stateMachine, exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void stateChangedInRelay() {
|
||||
// TODO: this is a temporary tweak to know when state is
|
||||
// changed in a submachine/regions order to give
|
||||
@@ -255,6 +265,11 @@ public abstract class StateMachineObjectSupport<S, E> extends LifecycleObjectSup
|
||||
stateListener.stateMachineStopped(stateMachine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stateMachineError(StateMachine<S, E> stateMachine, Exception exception) {
|
||||
stateListener.stateMachineError(stateMachine, exception);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* Copyright 2015 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;
|
||||
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.statemachine.access.StateMachineAccess;
|
||||
import org.springframework.statemachine.access.StateMachineFunction;
|
||||
import org.springframework.statemachine.config.EnableStateMachine;
|
||||
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
|
||||
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
|
||||
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
||||
import org.springframework.statemachine.event.OnStateMachineError;
|
||||
import org.springframework.statemachine.event.StateMachineEvent;
|
||||
import org.springframework.statemachine.listener.StateMachineListenerAdapter;
|
||||
import org.springframework.statemachine.support.StateMachineInterceptorAdapter;
|
||||
|
||||
/**
|
||||
* Tests for various errors and error handling.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*
|
||||
*/
|
||||
public class StateMachineErrorTests extends AbstractStateMachineTests {
|
||||
|
||||
@Override
|
||||
protected AnnotationConfigApplicationContext buildContext() {
|
||||
return new AnnotationConfigApplicationContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEvents() throws Exception {
|
||||
context.register(Config.class, Config1.class);
|
||||
context.refresh();
|
||||
|
||||
TestApplicationEventListener listener1 = context.getBean(TestApplicationEventListener.class);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ObjectStateMachine<TestStates,TestEvents> machine =
|
||||
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
|
||||
|
||||
assertThat(machine.hasStateMachineError(), is(false));
|
||||
|
||||
TestStateMachineListener listener2 = new TestStateMachineListener();
|
||||
machine.addStateListener(listener2);
|
||||
|
||||
machine.start();
|
||||
machine.setStateMachineError(new RuntimeException("myerror"));
|
||||
|
||||
assertThat(listener1.latch.await(1, TimeUnit.SECONDS), is(true));
|
||||
assertThat(listener1.count, is(1));
|
||||
assertThat(listener2.latch.await(1, TimeUnit.SECONDS), is(true));
|
||||
assertThat(listener2.count, is(1));
|
||||
assertThat(machine.hasStateMachineError(), is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInterceptHandlesError() throws Exception {
|
||||
context.register(Config.class, Config1.class);
|
||||
context.refresh();
|
||||
|
||||
TestApplicationEventListener listener1 = context.getBean(TestApplicationEventListener.class);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ObjectStateMachine<TestStates,TestEvents> machine =
|
||||
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
|
||||
|
||||
assertThat(machine.hasStateMachineError(), is(false));
|
||||
|
||||
machine.getStateMachineAccessor().doWithRegion(new StateMachineFunction<StateMachineAccess<TestStates,TestEvents>>() {
|
||||
|
||||
@Override
|
||||
public void apply(StateMachineAccess<TestStates, TestEvents> function) {
|
||||
function.addStateMachineInterceptor(new StateMachineInterceptorAdapter<TestStates,TestEvents>() {
|
||||
@Override
|
||||
public Exception stateMachineError(StateMachine<TestStates, TestEvents> stateMachine,
|
||||
Exception exception) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
TestStateMachineListener listener2 = new TestStateMachineListener();
|
||||
machine.addStateListener(listener2);
|
||||
|
||||
machine.start();
|
||||
machine.setStateMachineError(new RuntimeException("myerror"));
|
||||
|
||||
assertThat(listener1.latch.await(1, TimeUnit.SECONDS), is(false));
|
||||
assertThat(listener1.count, is(0));
|
||||
assertThat(listener2.latch.await(1, TimeUnit.SECONDS), is(false));
|
||||
assertThat(listener2.count, is(0));
|
||||
assertThat(machine.hasStateMachineError(), is(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorActive() throws Exception {
|
||||
context.register(Config1.class);
|
||||
context.refresh();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ObjectStateMachine<TestStates,TestEvents> machine =
|
||||
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
|
||||
|
||||
assertThat(machine.hasStateMachineError(), is(false));
|
||||
machine.start();
|
||||
machine.setStateMachineError(new RuntimeException("myerror"));
|
||||
assertThat(machine.hasStateMachineError(), is(true));
|
||||
assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S1));
|
||||
machine.sendEvent(TestEvents.E1);
|
||||
assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S1));
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine
|
||||
static class Config1 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
|
||||
states
|
||||
.withStates()
|
||||
.initial(TestStates.S1)
|
||||
.state(TestStates.S2)
|
||||
.state(TestStates.S3)
|
||||
.state(TestStates.S4);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
|
||||
transitions
|
||||
.withExternal()
|
||||
.source(TestStates.S1)
|
||||
.target(TestStates.S2)
|
||||
.event(TestEvents.E1)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(TestStates.S2)
|
||||
.target(TestStates.S3)
|
||||
.event(TestEvents.E2)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(TestStates.S3)
|
||||
.target(TestStates.S4)
|
||||
.event(TestEvents.E3)
|
||||
.and()
|
||||
.withExternal()
|
||||
.source(TestStates.S4)
|
||||
.target(TestStates.S3)
|
||||
.event(TestEvents.E4);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public TestApplicationEventListener testApplicationEventListener() {
|
||||
return new TestApplicationEventListener();
|
||||
}
|
||||
}
|
||||
|
||||
static class TestStateMachineListener extends StateMachineListenerAdapter<TestStates, TestEvents> {
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
int count = 0;
|
||||
|
||||
@Override
|
||||
public void stateMachineError(StateMachine<TestStates, TestEvents> stateMachine, Exception exception) {
|
||||
count++;
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
static class TestApplicationEventListener implements ApplicationListener<StateMachineEvent> {
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
int count = 0;
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(StateMachineEvent event) {
|
||||
if (event instanceof OnStateMachineError) {
|
||||
count++;
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -142,6 +142,15 @@ public class StateMachineAccessTests {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStateMachineError(Exception exception) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStateMachineError() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addStateListener(StateMachineListener<String, String> listener) {
|
||||
}
|
||||
|
||||
@@ -866,6 +866,12 @@ public class DocsConfigurationSampleTests extends AbstractStateMachineTests {
|
||||
public void postStateChange(State<String, String> state, Message<String> message,
|
||||
Transition<String, String> transition, StateMachine<String, String> stateMachine) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Exception stateMachineError(StateMachine<String, String> stateMachine,
|
||||
Exception exception) {
|
||||
return exception;
|
||||
}
|
||||
});
|
||||
// end::snippetZH[]
|
||||
}
|
||||
|
||||
@@ -176,6 +176,10 @@ public class ListenerTests extends AbstractStateMachineTests {
|
||||
stopLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stateMachineError(StateMachine<TestStates, TestEvents> stateMachine, Exception exception) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
|
||||
@@ -301,6 +301,11 @@ public class StateChangeInterceptorTests extends AbstractStateMachineTests {
|
||||
preStateChangeCount = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Exception stateMachineError(StateMachine<States, Events> stateMachine, Exception exception) {
|
||||
return exception;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -176,6 +176,15 @@ public class StateContextExpressionMethodsTests {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStateMachineError(Exception exception) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStateMachineError() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addStateListener(StateMachineListener<SpelStates, SpelEvents> listener) {
|
||||
}
|
||||
|
||||
@@ -651,6 +651,15 @@ public class ZookeeperStateMachineEnsembleTests extends AbstractZookeeperTests {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStateMachineError(Exception exception) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStateMachineError() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addStateListener(StateMachineListener<String, String> listener) {
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user