diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java index ba850f9b..1327dc27 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/AbstractState.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2020 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. @@ -466,9 +466,9 @@ public abstract class AbstractState extends LifecycleObjectSupport impleme final AtomicInteger completionCount = new AtomicInteger(stateActions.size()); Long timeout = resolveDoActionTimeout(context); return Flux.fromIterable(stateActions) - .map(stateAction -> executeAction(stateAction, context)) - .map(function -> { - return function + .doOnNext(stateAction -> { + executeAction(stateAction, context) + .onErrorResume(t -> Mono.empty()) .subscribeOn(Schedulers.parallel()) .doOnSubscribe(subscription -> { if (log.isDebugEnabled()) { @@ -479,13 +479,13 @@ public abstract class AbstractState extends LifecycleObjectSupport impleme .then(handleCompleteOrEmpty1(context, completionCount)) .subscribe(); }) - .then(handleCompleteOrEmpty2(context, completionCount)) - ; + .then(handleCompleteOrEmpty2(context, completionCount)); }); } private Mono handleCompleteOrEmpty1(StateContext context, AtomicInteger completionCount) { return Mono.defer(() -> { + log.debug("handleCompleteOrEmpty1 " + completionCount + " " + stateActions); if (completionCount.decrementAndGet() <= 0 && stateActions.size() > 0) { return handleStateDoOnComplete(context) .then(Mono.fromRunnable(() -> notifyStateOnComplete(context))); diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ObjectState.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ObjectState.java index 3fe3e163..2623289c 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ObjectState.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/state/ObjectState.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2020 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. @@ -146,9 +146,7 @@ public class ObjectState extends AbstractSimpleState { public Mono exit(StateContext context) { Mono actions = Flux.fromIterable(getExitActions()) .flatMap(a -> executeAction(a, context)) - .onErrorContinue((t, u) -> { - // TODO: REACTOR allow continue and fix with error handling overhaul - }) + .onErrorResume(t -> Mono.empty()) .then(); return super.exit(context).and(actions); } @@ -157,9 +155,7 @@ public class ObjectState extends AbstractSimpleState { public Mono entry(StateContext context) { Mono actions = Flux.fromIterable(getEntryActions()) .flatMap(a -> executeAction(a, context)) - .onErrorContinue((t, u) -> { - // TODO: REACTOR allow continue and fix with error handling overhaul - }) + .onErrorResume(t -> Mono.empty()) .then(); return actions.and(super.entry(context)); } diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java index 310c9e40..d710ce84 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/AbstractStateMachine.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2020 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. @@ -619,6 +619,7 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo } return Mono.just(message) .map(m -> getStateMachineInterceptors().preEvent(m, this)) + // .onErrorResume(error -> Mono.empty()) .onErrorResume(error -> Mono.empty()) .flatMapMany(m -> acceptEvent(m)) .doOnNext(notifyOnDenied()); @@ -642,29 +643,41 @@ public abstract class AbstractStateMachine extends StateMachineObjectSuppo stateMachineExecutor.queueDeferredEvent(message); return Flux.just(StateMachineEventResult.from(this, message, ResultType.DEFERRED)); } - return cs.sendEvent(message).collectList().flatMapMany(l -> { - Flux> ret = Flux.fromIterable(l); - if (!l.stream().anyMatch(er -> er.getResultType() == ResultType.ACCEPTED)) { - Mono> result = Flux.fromIterable(transitions) - .filter(transition -> cs != null && transition.getTrigger() != null) - .filter(transition -> StateMachineUtils.containsAtleastOne(transition.getSource().getIds(), cs.getIds())) - .flatMap(transition -> { - return Mono.from(transition.getTrigger().evaluate(triggerContext)) - .flatMap(e -> { - if (e) { - return stateMachineExecutor.queueEvent(Mono.just(message)) - .thenReturn(StateMachineEventResult.from(this, message, ResultType.ACCEPTED)); - } else { - return Mono.empty(); - } - }); - }) - .next() - .switchIfEmpty(Mono.just(StateMachineEventResult.from(this, message, ResultType.DENIED))); - ret = ret.concatWith(result); - } - return ret; - }); + + return cs.sendEvent(message) + .collectList() + .flatMapMany(l -> { + Flux> ret = Flux.fromIterable(l); + if (!l.stream().anyMatch(er -> er.getResultType() == ResultType.ACCEPTED)) { + Mono> result = Flux.fromIterable(transitions) + .filter(transition -> cs != null && transition.getTrigger() != null) + .filter(transition -> StateMachineUtils.containsAtleastOne(transition.getSource().getIds(), cs.getIds())) + .flatMap(transition -> { + return Mono.from(transition.getTrigger().evaluate(triggerContext)) + .flatMap(e -> { + if (e) { + return stateMachineExecutor.queueEvent(Mono.just(message)) + .then(Mono.defer(() -> { + return Mono.just(StateMachineEventResult.from(this, message, ResultType.ACCEPTED)); + })) + .onErrorResume(t -> { + return Mono.defer(() -> { + return Mono.just(StateMachineEventResult.from(this, message, ResultType.DENIED)); + }); + }); + } else { + return Mono.empty(); + } + }); + }) + .next() + .switchIfEmpty(Mono.defer(() -> { + return Mono.just(StateMachineEventResult.from(this, message, ResultType.DENIED)); + })); + ret = ret.concatWith(result); + } + return ret; + }); } return Flux.just(StateMachineEventResult.from(this, message, ResultType.DENIED)); }); diff --git a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/ReactiveStateMachineExecutor.java b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/ReactiveStateMachineExecutor.java index 137af75d..41b2c305 100644 --- a/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/ReactiveStateMachineExecutor.java +++ b/spring-statemachine-core/src/main/java/org/springframework/statemachine/support/ReactiveStateMachineExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2020 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. @@ -36,6 +36,7 @@ import org.springframework.messaging.MessageHeaders; import org.springframework.statemachine.StateContext; import org.springframework.statemachine.StateContext.Stage; import org.springframework.statemachine.StateMachine; +import org.springframework.statemachine.StateMachineException; import org.springframework.statemachine.StateMachineSystemConstants; import org.springframework.statemachine.state.JoinPseudoState; import org.springframework.statemachine.state.PseudoStateKind; @@ -200,7 +201,11 @@ public class ReactiveStateMachineExecutor extends LifecycleObjectSupport i return messages .flatMap(m -> handleEvent(m)) .doOnNext(i -> { - triggerSink.next(i); + try { + triggerSink.next(i); + } catch (Exception e) { + throw new StateMachineException("Unable to handle queued event", e); + } }) .then(); } @@ -325,8 +330,8 @@ public class ReactiveStateMachineExecutor extends LifecycleObjectSupport i } private Mono handleTriggerTrans(List> trans, Message queuedMessage, State completion) { - return Flux.fromIterable(trans) - .filter(t -> { + return Flux.fromIterable(trans) + .filter(t -> { State source = t.getSource(); if (source == null) { return false; @@ -368,8 +373,7 @@ public class ReactiveStateMachineExecutor extends LifecycleObjectSupport i .doFinally(s -> { joinSyncTransitions.clear(); }) - .then(Mono.just(true)) - ; + .then(Mono.just(true)); } else { return Mono.just(false); } @@ -388,16 +392,16 @@ public class ReactiveStateMachineExecutor extends LifecycleObjectSupport i .onErrorResume(e -> { interceptors.postTransition(stateContext); return Mono.just(false); - }); + }) + ; } else { return Mono.just(false); } }) - ) - .onErrorResume(e -> Mono.just(false)); + ); } }) - .takeUntil(x -> x) + .takeUntil(transit -> transit) .last(false); } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/StateMachineErrorTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/StateMachineErrorTests.java index c4d9b3dd..56e60071 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/StateMachineErrorTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/StateMachineErrorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2020 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. @@ -15,9 +15,7 @@ */ package org.springframework.statemachine; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.is; +import static org.assertj.core.api.Assertions.assertThat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -30,6 +28,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.messaging.Message; import org.springframework.statemachine.config.EnableStateMachine; import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; +import org.springframework.statemachine.config.StateMachineConfigurerAdapter; import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; import org.springframework.statemachine.event.OnStateMachineError; @@ -48,425 +47,464 @@ import org.springframework.statemachine.transition.Transition; */ public class StateMachineErrorTests extends AbstractStateMachineTests { - @Override - protected AnnotationConfigApplicationContext buildContext() { - return new AnnotationConfigApplicationContext(); - } - - @Test - public void testEvents() throws Exception { - context.register(EventListenerConfig1.class, Config1.class); - context.refresh(); - - TestApplicationEventListener1 listener1 = context.getBean(TestApplicationEventListener1.class); - TestApplicationEventListener2 listener3 = context.getBean(TestApplicationEventListener2.class); - - @SuppressWarnings("unchecked") - ObjectStateMachine 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(listener3.latch.await(1, TimeUnit.SECONDS), is(true)); - assertThat(listener3.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(EventListenerConfig1.class, Config1.class); - context.refresh(); - - TestApplicationEventListener1 listener1 = context.getBean(TestApplicationEventListener1.class); - - @SuppressWarnings("unchecked") - ObjectStateMachine machine = - context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); - - assertThat(machine.hasStateMachineError(), is(false)); - - machine.getStateMachineAccessor().doWithRegion( - function -> function.addStateMachineInterceptor(new StateMachineInterceptorAdapter() { - @Override - public Exception stateMachineError(StateMachine 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 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)); - } - - @Test - public void testListenerErrorsCauseNoMalfunction() throws Exception { - context.register(EventListenerConfig2.class, Config1.class); - context.refresh(); - - @SuppressWarnings("unchecked") - ObjectStateMachine machine = - context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); - StartedStateMachineListener listener1 = new StartedStateMachineListener(); - ErroringStateMachineListener listener2 = new ErroringStateMachineListener(); - StateChangedStateMachineListener listener3 = new StateChangedStateMachineListener(); - machine.addStateListener(listener1); - machine.addStateListener(listener2); - - machine.start(); - assertThat(listener1.latch.await(2, TimeUnit.SECONDS), is(true)); - machine.addStateListener(listener3); - machine.sendEvent(TestEvents.E1); - assertThat(listener3.latch.await(2, TimeUnit.SECONDS), is(true)); - assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S2)); - } - - @Test - public void testListenerErrorsCauseNoMalfunction2() throws Exception { - context.register(EventListenerConfig2.class, Config1.class); - context.refresh(); - - @SuppressWarnings("unchecked") - ObjectStateMachine machine = - context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); - StartedStateMachineListener listener1 = new StartedStateMachineListener(); - ErroringStateMachineListener2 listener2 = new ErroringStateMachineListener2(); - StateChangedStateMachineListener listener3 = new StateChangedStateMachineListener(); - machine.addStateListener(listener1); - machine.addStateListener(listener2); - - machine.start(); - assertThat(listener1.latch.await(2, TimeUnit.SECONDS), is(true)); - machine.addStateListener(listener3); - machine.sendEvent(TestEvents.E1); - assertThat(listener3.latch.await(2, TimeUnit.SECONDS), is(true)); - assertThat(machine.getState().getIds(), containsInAnyOrder(TestStates.S2)); - } - - @Configuration - @EnableStateMachine - static class Config1 extends EnumStateMachineConfigurerAdapter { - - @Override - public void configure(StateMachineStateConfigurer states) throws Exception { - states - .withStates() - .initial(TestStates.S1) - .state(TestStates.S2) - .state(TestStates.S3) - .state(TestStates.S4); - } - - @Override - public void configure(StateMachineTransitionConfigurer 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 - @EnableStateMachine - static class Config2 extends EnumStateMachineConfigurerAdapter { - - @Override - public void configure(StateMachineStateConfigurer states) throws Exception { - states - .withStates() - .initial(TestStates.S1) - .state(TestStates.S2) - .state(TestStates.S3); - } - - @Override - public void configure(StateMachineTransitionConfigurer 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); - } - - } - - @Configuration - static class EventListenerConfig1 { - - @Bean - public TestApplicationEventListener1 testApplicationEventListener1() { - return new TestApplicationEventListener1(); - } - - @Bean - public TestApplicationEventListener2 testApplicationEventListener2() { - return new TestApplicationEventListener2(); - } - } - - @Configuration - static class EventListenerConfig2 { - - @Bean - public ErroringApplicationEventListener1 erroringApplicationEventListener1() { - return new ErroringApplicationEventListener1(); - } - } - - static class TestStateMachineListener extends StateMachineListenerAdapter { - - CountDownLatch latch = new CountDownLatch(1); - int count = 0; - - @Override - public void stateMachineError(StateMachine stateMachine, Exception exception) { - count++; - latch.countDown(); - } - } - - static class TestApplicationEventListener1 implements ApplicationListener { - - CountDownLatch latch = new CountDownLatch(1); - int count = 0; - - @Override - public void onApplicationEvent(StateMachineEvent event) { - if (event instanceof OnStateMachineError) { - count++; - latch.countDown(); - } - } - } - - static class TestApplicationEventListener2 implements ApplicationListener { - - CountDownLatch latch = new CountDownLatch(1); - int count = 0; - - @Override - public void onApplicationEvent(OnStateMachineError event) { - count++; - latch.countDown(); - } - - } - - static class ErroringApplicationEventListener1 implements ApplicationListener { - - @Override - public void onApplicationEvent(StateMachineEvent event) { - throw new RuntimeException(); - } - } - - - static class StateChangedStateMachineListener extends StateMachineListenerAdapter { - - CountDownLatch latch = new CountDownLatch(1); - - @Override - public void stateChanged(State from, State to) { - latch.countDown(); - } - - void reset(int a) { - latch = new CountDownLatch(a); - } - } - - static class StartedStateMachineListener extends StateMachineListenerAdapter { - - CountDownLatch latch = new CountDownLatch(1); - - @Override - public void stateMachineStarted(StateMachine stateMachine) { - latch.countDown(); - } - } - - static class ErroringStateMachineListener implements StateMachineListener { - - @Override - public void stateChanged(State from, State to) { - throw new RuntimeException(); - } - - @Override - public void stateEntered(State state) { - throw new RuntimeException(); - } - - @Override - public void stateExited(State state) { - throw new RuntimeException(); - } - - @Override - public void eventNotAccepted(Message event) { - throw new RuntimeException(); - } - - @Override - public void transition(Transition transition) { - throw new RuntimeException(); - } - - @Override - public void transitionStarted(Transition transition) { - throw new RuntimeException(); - } - - @Override - public void transitionEnded(Transition transition) { - throw new RuntimeException(); - } - - @Override - public void stateMachineStarted(StateMachine stateMachine) { - throw new RuntimeException(); - } - - @Override - public void stateMachineStopped(StateMachine stateMachine) { - throw new RuntimeException(); - } - - @Override - public void stateMachineError(StateMachine stateMachine, Exception exception) { - throw new RuntimeException(); - } - - @Override - public void extendedStateChanged(Object key, Object value) { - throw new RuntimeException(); - } - - @Override - public void stateContext(StateContext stateContext) { - throw new RuntimeException(); - } - } - - static class ErroringStateMachineListener2 implements StateMachineListener { - - @Override - public void stateChanged(State from, State to) { - throw new Error(); - } - - @Override - public void stateEntered(State state) { - throw new Error(); - } - - @Override - public void stateExited(State state) { - throw new Error(); - } - - @Override - public void eventNotAccepted(Message event) { - throw new Error(); - } - - @Override - public void transition(Transition transition) { - throw new Error(); - } - - @Override - public void transitionStarted(Transition transition) { - throw new Error(); - } - - @Override - public void transitionEnded(Transition transition) { - throw new Error(); - } - - @Override - public void stateMachineStarted(StateMachine stateMachine) { - throw new Error(); - } - - @Override - public void stateMachineStopped(StateMachine stateMachine) { - throw new Error(); - } - - @Override - public void stateMachineError(StateMachine stateMachine, Exception exception) { - throw new Error(); - } - - @Override - public void extendedStateChanged(Object key, Object value) { - throw new Error(); - } - - @Override - public void stateContext(StateContext stateContext) { - throw new Error(); - } - } - + @Override + protected AnnotationConfigApplicationContext buildContext() { + return new AnnotationConfigApplicationContext(); + } + + @Test + public void testEvents() throws Exception { + context.register(EventListenerConfig1.class, Config1.class); + context.refresh(); + + TestApplicationEventListener1 listener1 = context.getBean(TestApplicationEventListener1.class); + TestApplicationEventListener2 listener3 = context.getBean(TestApplicationEventListener2.class); + + @SuppressWarnings("unchecked") + ObjectStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + + assertThat(machine.hasStateMachineError()).isFalse(); + + TestStateMachineListener listener2 = new TestStateMachineListener(); + machine.addStateListener(listener2); + + machine.start(); + machine.setStateMachineError(new RuntimeException("myerror")); + + assertThat(listener1.latch.await(1, TimeUnit.SECONDS)).isTrue(); + assertThat(listener1.count).isEqualTo(1); + assertThat(listener3.latch.await(1, TimeUnit.SECONDS)).isTrue(); + assertThat(listener3.count).isEqualTo(1); + assertThat(listener2.latch.await(1, TimeUnit.SECONDS)).isTrue(); + assertThat(listener2.count).isEqualTo(1); + assertThat(machine.hasStateMachineError()).isTrue(); + } + + @Test + public void testInterceptHandlesError() throws Exception { + context.register(EventListenerConfig1.class, Config1.class); + context.refresh(); + + TestApplicationEventListener1 listener1 = context.getBean(TestApplicationEventListener1.class); + + @SuppressWarnings("unchecked") + ObjectStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + + assertThat(machine.hasStateMachineError()).isFalse(); + + machine.getStateMachineAccessor().doWithRegion( + function -> function.addStateMachineInterceptor(new StateMachineInterceptorAdapter() { + @Override + public Exception stateMachineError(StateMachine 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)).isFalse(); + assertThat(listener1.count).isEqualTo(0); + assertThat(listener2.latch.await(1, TimeUnit.SECONDS)).isFalse(); + assertThat(listener2.count).isEqualTo(0); + assertThat(machine.hasStateMachineError()).isFalse(); + } + + @Test + public void testErrorActive() throws Exception { + context.register(Config1.class); + context.refresh(); + + @SuppressWarnings("unchecked") + ObjectStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + + assertThat(machine.hasStateMachineError()).isFalse(); + machine.start(); + machine.setStateMachineError(new RuntimeException("myerror")); + assertThat(machine.hasStateMachineError()).isTrue(); + assertThat(machine.getState().getIds()).containsExactlyInAnyOrder(TestStates.S1); + machine.sendEvent(TestEvents.E1); + assertThat(machine.getState().getIds()).containsExactlyInAnyOrder(TestStates.S1); + } + + @Test + public void testListenerErrorsCauseNoMalfunction() throws Exception { + context.register(EventListenerConfig2.class, Config1.class); + context.refresh(); + + @SuppressWarnings("unchecked") + ObjectStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + StartedStateMachineListener listener1 = new StartedStateMachineListener(); + ErroringStateMachineListener listener2 = new ErroringStateMachineListener(); + StateChangedStateMachineListener listener3 = new StateChangedStateMachineListener(); + machine.addStateListener(listener1); + machine.addStateListener(listener2); + + machine.start(); + assertThat(listener1.latch.await(2, TimeUnit.SECONDS)).isTrue(); + machine.addStateListener(listener3); + machine.sendEvent(TestEvents.E1); + assertThat(listener3.latch.await(2, TimeUnit.SECONDS)).isTrue(); + assertThat(machine.getState().getIds()).containsExactlyInAnyOrder(TestStates.S2); + } + + @Test + public void testListenerErrorsCauseNoMalfunction2() throws Exception { + context.register(EventListenerConfig2.class, Config1.class); + context.refresh(); + + @SuppressWarnings("unchecked") + ObjectStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + StartedStateMachineListener listener1 = new StartedStateMachineListener(); + ErroringStateMachineListener2 listener2 = new ErroringStateMachineListener2(); + StateChangedStateMachineListener listener3 = new StateChangedStateMachineListener(); + machine.addStateListener(listener1); + machine.addStateListener(listener2); + + machine.start(); + assertThat(listener1.latch.await(2, TimeUnit.SECONDS)).isTrue(); + machine.addStateListener(listener3); + machine.sendEvent(TestEvents.E1); + assertThat(listener3.latch.await(2, TimeUnit.SECONDS)).isTrue(); + assertThat(machine.getState().getIds()).containsExactlyInAnyOrder(TestStates.S2); + } + + @Configuration + @EnableStateMachine + static class Config1 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S2) + .state(TestStates.S3) + .state(TestStates.S4); + } + + @Override + public void configure(StateMachineTransitionConfigurer 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 + @EnableStateMachine + static class Config2 extends EnumStateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial(TestStates.S1) + .state(TestStates.S2) + .state(TestStates.S3); + } + + @Override + public void configure(StateMachineTransitionConfigurer 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); + } + + } + + @Configuration + static class EventListenerConfig1 { + + @Bean + public TestApplicationEventListener1 testApplicationEventListener1() { + return new TestApplicationEventListener1(); + } + + @Bean + public TestApplicationEventListener2 testApplicationEventListener2() { + return new TestApplicationEventListener2(); + } + } + + @Configuration + static class EventListenerConfig2 { + + @Bean + public ErroringApplicationEventListener1 erroringApplicationEventListener1() { + return new ErroringApplicationEventListener1(); + } + } + + static class TestStateMachineListener extends StateMachineListenerAdapter { + + CountDownLatch latch = new CountDownLatch(1); + int count = 0; + + @Override + public void stateMachineError(StateMachine stateMachine, Exception exception) { + count++; + latch.countDown(); + } + } + + static class TestApplicationEventListener1 implements ApplicationListener { + + CountDownLatch latch = new CountDownLatch(1); + int count = 0; + + @Override + public void onApplicationEvent(StateMachineEvent event) { + if (event instanceof OnStateMachineError) { + count++; + latch.countDown(); + } + } + } + + static class TestApplicationEventListener2 implements ApplicationListener { + + CountDownLatch latch = new CountDownLatch(1); + int count = 0; + + @Override + public void onApplicationEvent(OnStateMachineError event) { + count++; + latch.countDown(); + } + + } + + static class ErroringApplicationEventListener1 implements ApplicationListener { + + @Override + public void onApplicationEvent(StateMachineEvent event) { + throw new RuntimeException(); + } + } + + + static class StateChangedStateMachineListener extends StateMachineListenerAdapter { + + CountDownLatch latch = new CountDownLatch(1); + + @Override + public void stateChanged(State from, State to) { + latch.countDown(); + } + + void reset(int a) { + latch = new CountDownLatch(a); + } + } + + static class StartedStateMachineListener extends StateMachineListenerAdapter { + + CountDownLatch latch = new CountDownLatch(1); + + @Override + public void stateMachineStarted(StateMachine stateMachine) { + latch.countDown(); + } + } + + static class ErroringStateMachineListener implements StateMachineListener { + + @Override + public void stateChanged(State from, State to) { + throw new RuntimeException(); + } + + @Override + public void stateEntered(State state) { + throw new RuntimeException(); + } + + @Override + public void stateExited(State state) { + throw new RuntimeException(); + } + + @Override + public void eventNotAccepted(Message event) { + throw new RuntimeException(); + } + + @Override + public void transition(Transition transition) { + throw new RuntimeException(); + } + + @Override + public void transitionStarted(Transition transition) { + throw new RuntimeException(); + } + + @Override + public void transitionEnded(Transition transition) { + throw new RuntimeException(); + } + + @Override + public void stateMachineStarted(StateMachine stateMachine) { + throw new RuntimeException(); + } + + @Override + public void stateMachineStopped(StateMachine stateMachine) { + throw new RuntimeException(); + } + + @Override + public void stateMachineError(StateMachine stateMachine, Exception exception) { + throw new RuntimeException(); + } + + @Override + public void extendedStateChanged(Object key, Object value) { + throw new RuntimeException(); + } + + @Override + public void stateContext(StateContext stateContext) { + throw new RuntimeException(); + } + } + + static class ErroringStateMachineListener2 implements StateMachineListener { + + @Override + public void stateChanged(State from, State to) { + throw new Error(); + } + + @Override + public void stateEntered(State state) { + throw new Error(); + } + + @Override + public void stateExited(State state) { + throw new Error(); + } + + @Override + public void eventNotAccepted(Message event) { + throw new Error(); + } + + @Override + public void transition(Transition transition) { + throw new Error(); + } + + @Override + public void transitionStarted(Transition transition) { + throw new Error(); + } + + @Override + public void transitionEnded(Transition transition) { + throw new Error(); + } + + @Override + public void stateMachineStarted(StateMachine stateMachine) { + throw new Error(); + } + + @Override + public void stateMachineStopped(StateMachine stateMachine) { + throw new Error(); + } + + @Override + public void stateMachineError(StateMachine stateMachine, Exception exception) { + throw new Error(); + } + + @Override + public void extendedStateChanged(Object key, Object value) { + throw new Error(); + } + + @Override + public void stateContext(StateContext stateContext) { + throw new Error(); + } + } + + @SuppressWarnings("unchecked") + @Test + public void testActionEntryError() throws Exception { + context.register(Config3.class); + context.refresh(); + + ObjectStateMachine machine = + context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class); + machine.start(); + assertThat(machine.getState().getIds()).containsExactlyInAnyOrder("S2"); + } + + @Configuration + @EnableStateMachine + static class Config3 extends StateMachineConfigurerAdapter { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states + .withStates() + .initial("SI") + .state("S1") + .stateEntry("S2", (context) -> { + throw new RuntimeException("error"); + }); + } + + @Override + public void configure(StateMachineTransitionConfigurer transitions) throws Exception { + transitions + .withExternal() + .source("SI") + .target("S1") + .and() + .withExternal() + .source("S1") + .target("S2"); + } + } } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/TestUtils.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/TestUtils.java index 73e96745..db46bfb1 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/TestUtils.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/TestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2020 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. @@ -25,6 +25,7 @@ import java.lang.reflect.Method; import org.springframework.beans.factory.BeanFactory; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; +import org.springframework.statemachine.StateMachineEventResult.ResultType; import org.springframework.statemachine.action.Action; import org.springframework.statemachine.config.StateMachineFactory; import org.springframework.statemachine.persist.StateMachinePersister; @@ -36,7 +37,7 @@ import reactor.test.StepVerifier; /** * Utils for tests. - * + * * @author Janne Valkealahti * */ @@ -107,6 +108,22 @@ public class TestUtils { .verifyComplete(); } + public static void doSendEventAndConsumeResultAsDenied(StateMachine stateMachine, E event) { + StepVerifier.create(stateMachine.sendEvent(eventAsMono(event))) + .consumeNextWith(result -> { + assertThat(result.getResultType()).isEqualTo(ResultType.DENIED); + }) + .verifyComplete(); + } + + public static void doSendEventAndConsumeResultAsDenied(StateMachine stateMachine, Message event) { + StepVerifier.create(stateMachine.sendEvent(eventAsMono(event))) + .consumeNextWith(result -> { + assertThat(result.getResultType()).isEqualTo(ResultType.DENIED); + }) + .verifyComplete(); + } + @SuppressWarnings("unchecked") public static T readField(String name, Object target) throws Exception { Field field = null; @@ -169,5 +186,5 @@ public class TestUtils { method.setAccessible(true); return (T) ReflectionUtils.invokeMethod(method, target, args); } - + } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/AbstractSecurityTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/AbstractSecurityTests.java index 0c5ac547..29479b25 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/AbstractSecurityTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/AbstractSecurityTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2020 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. @@ -19,6 +19,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.is; import static org.springframework.statemachine.TestUtils.doSendEventAndConsumeAll; +import static org.springframework.statemachine.TestUtils.doSendEventAndConsumeResultAsDenied; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -60,6 +61,18 @@ public abstract class AbstractSecurityTests extends AbstractStateMachineTests { assertThat(machine.getState().getIds(), containsInAnyOrder(States.S0)); } + protected static void assertTransitionDeniedResultAsDenied(StateMachine machine, TestListener listener) throws Exception { + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true)); + assertThat(listener.stateChangedCount, is(1)); + assertThat(machine.getState().getIds(), containsInAnyOrder(States.S0)); + + listener.reset(1); + doSendEventAndConsumeResultAsDenied(machine, Events.A); + assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(false)); + assertThat(listener.stateChangedCount, is(0)); + assertThat(machine.getState().getIds(), containsInAnyOrder(States.S0)); + } + protected static enum States { S0, S1; } diff --git a/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/EventSecurityTests.java b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/EventSecurityTests.java index 5980f6f4..55a1022f 100644 --- a/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/EventSecurityTests.java +++ b/spring-statemachine-core/src/test/java/org/springframework/statemachine/security/EventSecurityTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2020 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. @@ -35,7 +35,7 @@ public class EventSecurityTests extends AbstractSecurityTests { public void testNoSecurityContext() throws Exception { TestListener listener = new TestListener(); StateMachine machine = buildMachine(listener, "ROLE_ANONYMOUS", ComparisonType.ANY, null); - assertTransitionDenied(machine, listener); + assertTransitionDeniedResultAsDenied(machine, listener); } @Test