diff --git a/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/StateMachine.java b/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/StateMachine.java index e99102f84..0e743a0d0 100644 --- a/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/StateMachine.java +++ b/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/StateMachine.java @@ -12,6 +12,14 @@ */ public interface StateMachine extends Visitable{ + /** + * Verify if an event {@code E} can be fired from current state {@code S} + * @param sourceStateId + * @param event + * @return + */ + boolean verify(S sourceStateId,E event); + /** * Send an event {@code E} to the state machine. * diff --git a/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/builder/AlertFailCallback.java b/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/builder/AlertFailCallback.java new file mode 100644 index 000000000..56a7290ed --- /dev/null +++ b/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/builder/AlertFailCallback.java @@ -0,0 +1,19 @@ +package com.alibaba.cola.statemachine.builder; + +import com.alibaba.cola.statemachine.exception.TransitionFailException; + +/** + * Alert fail callback, throw an {@code TransitionFailException} + * + * @author 龙也 + * @date 2022/9/15 12:02 PM + */ +public class AlertFailCallback implements FailCallback { + + @Override + public void onFail(S sourceState, E event, C context) { + throw new TransitionFailException( + "Cannot fire event [" + event + "] on current state [" + sourceState + "] with context [" + context + "]" + ); + } +} diff --git a/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/builder/FailCallback.java b/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/builder/FailCallback.java new file mode 100644 index 000000000..716642a71 --- /dev/null +++ b/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/builder/FailCallback.java @@ -0,0 +1,20 @@ +package com.alibaba.cola.statemachine.builder; + +/** + * FailCallback + * + * @author 龙也 + * @date 2022/9/15 12:02 PM + */ +@FunctionalInterface +public interface FailCallback { + + /** + * Callback function to execute if failed to trigger an Event + * + * @param sourceState + * @param event + * @param context + */ + void onFail(S sourceState, E event, C context); +} diff --git a/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/builder/NumbFailCallback.java b/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/builder/NumbFailCallback.java new file mode 100644 index 000000000..943cd6826 --- /dev/null +++ b/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/builder/NumbFailCallback.java @@ -0,0 +1,15 @@ +package com.alibaba.cola.statemachine.builder; + +/** + * Default fail callback, do nothing. + * + * @author 龙也 + * @date 2022/9/15 12:02 PM + */ +public class NumbFailCallback implements FailCallback { + + @Override + public void onFail(S sourceState, E event, C context) { + //do nothing + } +} diff --git a/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/builder/StateMachineBuilder.java b/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/builder/StateMachineBuilder.java index 029433e0c..1b09ef410 100644 --- a/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/builder/StateMachineBuilder.java +++ b/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/builder/StateMachineBuilder.java @@ -11,22 +11,32 @@ public interface StateMachineBuilder { /** * Builder for one transition + * * @return External transition builder */ ExternalTransitionBuilder externalTransition(); /** * Builder for multiple transitions + * * @return External transition builder */ ExternalTransitionsBuilder externalTransitions(); /** * Start to build internal transition + * * @return Internal transition builder */ InternalTransitionBuilder internalTransition(); - StateMachine build(String machineId); + /** + * set up fail callback, default do nothing {@code NumbFailCallbackImpl} + * + * @param callback + */ + void setFailCallback(FailCallback callback); + + StateMachine build(String machineId); } diff --git a/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/builder/StateMachineBuilderImpl.java b/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/builder/StateMachineBuilderImpl.java index 497cb9e3c..f08365e26 100644 --- a/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/builder/StateMachineBuilderImpl.java +++ b/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/builder/StateMachineBuilderImpl.java @@ -1,14 +1,14 @@ package com.alibaba.cola.statemachine.builder; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + import com.alibaba.cola.statemachine.State; import com.alibaba.cola.statemachine.StateMachine; import com.alibaba.cola.statemachine.StateMachineFactory; import com.alibaba.cola.statemachine.impl.StateMachineImpl; import com.alibaba.cola.statemachine.impl.TransitionType; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - /** * StateMachineBuilderImpl * @@ -20,8 +20,9 @@ public class StateMachineBuilderImpl implements StateMachineBuilder> stateMap = new ConcurrentHashMap<>(); + private final Map> stateMap = new ConcurrentHashMap<>(); private final StateMachineImpl stateMachine = new StateMachineImpl<>(stateMap); + private FailCallback failCallback = new NumbFailCallback<>(); @Override public ExternalTransitionBuilder externalTransition() { @@ -38,10 +39,16 @@ public InternalTransitionBuilder internalTransition() { return new TransitionBuilderImpl<>(stateMap, TransitionType.INTERNAL); } + @Override + public void setFailCallback(FailCallback callback) { + this.failCallback = callback; + } + @Override public StateMachine build(String machineId) { stateMachine.setMachineId(machineId); stateMachine.setReady(true); + stateMachine.setFailCallback(failCallback); StateMachineFactory.register(stateMachine); return stateMachine; } diff --git a/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/exception/TransitionFailException.java b/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/exception/TransitionFailException.java new file mode 100644 index 000000000..0512a4732 --- /dev/null +++ b/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/exception/TransitionFailException.java @@ -0,0 +1,12 @@ +package com.alibaba.cola.statemachine.exception; + +/** + * @author 龙也 + * @date 2022/9/15 12:08 PM + */ +public class TransitionFailException extends RuntimeException { + + public TransitionFailException(String errMsg) { + super(errMsg); + } +} diff --git a/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/impl/StateMachineImpl.java b/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/impl/StateMachineImpl.java index f36fa83d3..b930216f4 100644 --- a/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/impl/StateMachineImpl.java +++ b/cola-components/cola-component-statemachine/src/main/java/com/alibaba/cola/statemachine/impl/StateMachineImpl.java @@ -1,12 +1,13 @@ package com.alibaba.cola.statemachine.impl; +import java.util.List; +import java.util.Map; + import com.alibaba.cola.statemachine.State; import com.alibaba.cola.statemachine.StateMachine; import com.alibaba.cola.statemachine.Transition; import com.alibaba.cola.statemachine.Visitor; - -import java.util.List; -import java.util.Map; +import com.alibaba.cola.statemachine.builder.FailCallback; /** * For performance consideration, @@ -26,10 +27,23 @@ public class StateMachineImpl implements StateMachine { private boolean ready; + private FailCallback failCallback; + public StateMachineImpl(Map> stateMap) { this.stateMap = stateMap; } + @Override + public boolean verify(S sourceStateId, E event) { + isReady(); + + State sourceState = getState(sourceStateId); + + List> transitions = sourceState.getEventTransitions(event); + + return transitions != null && transitions.size() != 0; + } + @Override public S fireEvent(S sourceStateId, E event, C ctx) { isReady(); @@ -37,6 +51,7 @@ public S fireEvent(S sourceStateId, E event, C ctx) { if (transition == null) { Debugger.debug("There is no Transition for " + event); + failCallback.onFail(sourceStateId, event, ctx); return sourceStateId; } @@ -115,4 +130,8 @@ public void setMachineId(String machineId) { public void setReady(boolean ready) { this.ready = ready; } + + public void setFailCallback(FailCallback failCallback) { + this.failCallback = failCallback; + } } diff --git a/cola-components/cola-component-statemachine/src/test/java/com/alibaba/cola/test/StateMachineTest.java b/cola-components/cola-component-statemachine/src/test/java/com/alibaba/cola/test/StateMachineTest.java index 8959345a5..cbc39d423 100644 --- a/cola-components/cola-component-statemachine/src/test/java/com/alibaba/cola/test/StateMachineTest.java +++ b/cola-components/cola-component-statemachine/src/test/java/com/alibaba/cola/test/StateMachineTest.java @@ -4,8 +4,11 @@ import com.alibaba.cola.statemachine.Condition; import com.alibaba.cola.statemachine.StateMachine; import com.alibaba.cola.statemachine.StateMachineFactory; +import com.alibaba.cola.statemachine.builder.AlertFailCallback; import com.alibaba.cola.statemachine.builder.StateMachineBuilder; import com.alibaba.cola.statemachine.builder.StateMachineBuilderFactory; +import com.alibaba.cola.statemachine.exception.TransitionFailException; + import org.junit.Assert; import org.junit.Test; @@ -20,27 +23,34 @@ public class StateMachineTest { static String MACHINE_ID = "TestStateMachine"; static enum States { - STATE1, STATE2, STATE3, STATE4 + STATE1, + STATE2, + STATE3, + STATE4 } static enum Events { - EVENT1, EVENT2, EVENT3, EVENT4, INTERNAL_EVENT + EVENT1, + EVENT2, + EVENT3, + EVENT4, + INTERNAL_EVENT } - static class Context{ + static class Context { String operator = "frank"; String entityId = "123465"; } @Test - public void testExternalNormal(){ + public void testExternalNormal() { StateMachineBuilder builder = StateMachineBuilderFactory.create(); builder.externalTransition() - .from(States.STATE1) - .to(States.STATE2) - .on(Events.EVENT1) - .when(checkCondition()) - .perform(doAction()); + .from(States.STATE1) + .to(States.STATE2) + .on(Events.EVENT1) + .when(checkCondition()) + .perform(doAction()); StateMachine stateMachine = builder.build(MACHINE_ID); States target = stateMachine.fireEvent(States.STATE1, Events.EVENT1, new Context()); @@ -48,29 +58,62 @@ public void testExternalNormal(){ } @Test - public void testExternalTransitionsNormal(){ + public void testFail() { + StateMachineBuilder builder = StateMachineBuilderFactory.create(); + builder.externalTransition() + .from(States.STATE1) + .to(States.STATE2) + .on(Events.EVENT1) + .when(checkCondition()) + .perform(doAction()); + + builder.setFailCallback(new AlertFailCallback<>()); + + StateMachine stateMachine = builder.build(MACHINE_ID + "-testFail"); + Assert.assertThrows(TransitionFailException.class, + () -> stateMachine.fireEvent(States.STATE2, Events.EVENT1, new Context())); + } + + @Test + public void testVerify() { + StateMachineBuilder builder = StateMachineBuilderFactory.create(); + builder.externalTransition() + .from(States.STATE1) + .to(States.STATE2) + .on(Events.EVENT1) + .when(checkCondition()) + .perform(doAction()); + + StateMachine stateMachine = builder.build(MACHINE_ID + "-testVerify"); + + Assert.assertTrue(stateMachine.verify(States.STATE1, Events.EVENT1)); + Assert.assertFalse(stateMachine.verify(States.STATE1, Events.EVENT2)); + } + + @Test + public void testExternalTransitionsNormal() { StateMachineBuilder builder = StateMachineBuilderFactory.create(); builder.externalTransitions() - .fromAmong(States.STATE1, States.STATE2, States.STATE3) - .to(States.STATE4) - .on(Events.EVENT1) - .when(checkCondition()) - .perform(doAction()); + .fromAmong(States.STATE1, States.STATE2, States.STATE3) + .to(States.STATE4) + .on(Events.EVENT1) + .when(checkCondition()) + .perform(doAction()); - StateMachine stateMachine = builder.build(MACHINE_ID+"1"); + StateMachine stateMachine = builder.build(MACHINE_ID + "1"); States target = stateMachine.fireEvent(States.STATE2, Events.EVENT1, new Context()); Assert.assertEquals(States.STATE4, target); } @Test - public void testInternalNormal(){ + public void testInternalNormal() { StateMachineBuilder builder = StateMachineBuilderFactory.create(); builder.internalTransition() - .within(States.STATE1) - .on(Events.INTERNAL_EVENT) - .when(checkCondition()) - .perform(doAction()); - StateMachine stateMachine = builder.build(MACHINE_ID+"2"); + .within(States.STATE1) + .on(Events.INTERNAL_EVENT) + .when(checkCondition()) + .perform(doAction()); + StateMachine stateMachine = builder.build(MACHINE_ID + "2"); stateMachine.fireEvent(States.STATE1, Events.EVENT1, new Context()); States target = stateMachine.fireEvent(States.STATE1, Events.INTERNAL_EVENT, new Context()); @@ -78,7 +121,7 @@ public void testInternalNormal(){ } @Test - public void testExternalInternalNormal(){ + public void testExternalInternalNormal() { StateMachine stateMachine = buildStateMachine("testExternalInternalNormal"); Context context = new Context(); @@ -95,38 +138,38 @@ public void testExternalInternalNormal(){ private StateMachine buildStateMachine(String machineId) { StateMachineBuilder builder = StateMachineBuilderFactory.create(); builder.externalTransition() - .from(States.STATE1) - .to(States.STATE2) - .on(Events.EVENT1) - .when(checkCondition()) - .perform(doAction()); + .from(States.STATE1) + .to(States.STATE2) + .on(Events.EVENT1) + .when(checkCondition()) + .perform(doAction()); builder.internalTransition() - .within(States.STATE2) - .on(Events.INTERNAL_EVENT) - .when(checkCondition()) - .perform(doAction()); + .within(States.STATE2) + .on(Events.INTERNAL_EVENT) + .when(checkCondition()) + .perform(doAction()); builder.externalTransition() - .from(States.STATE2) - .to(States.STATE1) - .on(Events.EVENT2) - .when(checkCondition()) - .perform(doAction()); + .from(States.STATE2) + .to(States.STATE1) + .on(Events.EVENT2) + .when(checkCondition()) + .perform(doAction()); builder.externalTransition() - .from(States.STATE1) - .to(States.STATE3) - .on(Events.EVENT3) - .when(checkCondition()) - .perform(doAction()); + .from(States.STATE1) + .to(States.STATE3) + .on(Events.EVENT3) + .when(checkCondition()) + .perform(doAction()); builder.externalTransitions() - .fromAmong(States.STATE1, States.STATE2, States.STATE3) - .to(States.STATE4) - .on(Events.EVENT4) - .when(checkCondition()) - .perform(doAction()); + .fromAmong(States.STATE1, States.STATE2, States.STATE3) + .to(States.STATE4) + .on(Events.EVENT4) + .when(checkCondition()) + .perform(doAction()); builder.build(machineId); @@ -136,11 +179,11 @@ private StateMachine buildStateMachine(String machineId } @Test - public void testMultiThread(){ + public void testMultiThread() { buildStateMachine("testMultiThread"); - for(int i=0 ; i<10 ; i++){ - Thread thread = new Thread(()->{ + for (int i = 0; i < 10; i++) { + Thread thread = new Thread(() -> { StateMachine stateMachine = StateMachineFactory.get("testMultiThread"); States target = stateMachine.fireEvent(States.STATE1, Events.EVENT1, new Context()); Assert.assertEquals(States.STATE2, target); @@ -148,8 +191,7 @@ public void testMultiThread(){ thread.start(); } - - for(int i=0 ; i<10 ; i++) { + for (int i = 0; i < 10; i++) { Thread thread = new Thread(() -> { StateMachine stateMachine = StateMachineFactory.get("testMultiThread"); States target = stateMachine.fireEvent(States.STATE1, Events.EVENT4, new Context()); @@ -158,7 +200,7 @@ public void testMultiThread(){ thread.start(); } - for(int i=0 ; i<10 ; i++) { + for (int i = 0; i < 10; i++) { Thread thread = new Thread(() -> { StateMachine stateMachine = StateMachineFactory.get("testMultiThread"); States target = stateMachine.fireEvent(States.STATE1, Events.EVENT3, new Context()); @@ -169,20 +211,20 @@ public void testMultiThread(){ } - private Condition checkCondition() { return new Condition() { @Override public boolean isSatisfied(Context context) { - System.out.println("Check condition : "+context); + System.out.println("Check condition : " + context); return true; } }; } private Action doAction() { - return (from, to, event, ctx)->{ - System.out.println(ctx.operator+" is operating "+ctx.entityId+" from:"+from+" to:"+to+" on:"+event); + return (from, to, event, ctx) -> { + System.out.println( + ctx.operator + " is operating " + ctx.entityId + " from:" + from + " to:" + to + " on:" + event); }; }