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:
Janne Valkealahti
2015-07-31 13:10:47 +01:00
parent 6684b0deda
commit d5257c64e7
20 changed files with 461 additions and 2 deletions

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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));
}
}
}

View File

@@ -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 + "]";
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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) {
}
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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) {
}

View File

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

View File

@@ -176,6 +176,10 @@ public class ListenerTests extends AbstractStateMachineTests {
stopLatch.countDown();
}
@Override
public void stateMachineError(StateMachine<TestStates, TestEvents> stateMachine, Exception exception) {
}
}
@Configuration

View File

@@ -301,6 +301,11 @@ public class StateChangeInterceptorTests extends AbstractStateMachineTests {
preStateChangeCount = 0;
}
@Override
public Exception stateMachineError(StateMachine<States, Events> stateMachine, Exception exception) {
return exception;
}
}
}

View File

@@ -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) {
}

View File

@@ -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) {
}