From a864b5b19dfd5a9b7d4dde25b3b8862f0e28cef1 Mon Sep 17 00:00:00 2001 From: Pierre-Louis MELIN Date: Wed, 29 May 2024 15:05:45 +0200 Subject: [PATCH 1/2] feat(history): add a new type 'recursive' Closes #148 --- README.md | 6 +- .../foundation/fsm/HistoryType.java | 6 ++ .../foundation/fsm/ImmutableState.java | 11 ++- .../foundation/fsm/impl/DotVisitorImpl.java | 6 +- .../foundation/fsm/impl/StateImpl.java | 27 ++++++ .../fsm/impl/StateMachineImporterImpl.java | 2 + .../fsm/samples/RecursiveHistorySample.java | 83 +++++++++++++++++++ 7 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 squirrel-foundation/src/test/java/org/squirrelframework/foundation/fsm/samples/RecursiveHistorySample.java diff --git a/README.md b/README.md index a727f059..b1d0a7f8 100644 --- a/README.md +++ b/README.md @@ -501,7 +501,10 @@ To define the context event, user has two way, annotation or builder API. * **Using History States to Save and Restore the Current State** - The history pseudo-state allows a state machine to remember its state configuration. A transition taking the history state as its target will return the state machine to this recorded configuration. If the 'type' of a history is "shallow", the state machine processor must record the direct active children of its parent before taking any transition that exits the parent. If the 'type' of a history is "deep", the state machine processor must record all the active descendants of the parent before taking any transition that exits the parent. + The history pseudo-state allows a state machine to remember its state configuration. A transition taking the history state as its target will return the state machine to this recorded configuration. Different types of history are available: + - "shallow": the history pseudo-state represents the last active child of this composite state. + - "recursive": the history pseudo-state represents the last active child of the configuration of this composite state, including the nested composite states down to the last composite state with history type "recursive". + - "deep": the history pseudo-state represents the last active child of the whole configuration of this composite state, including the nested composite states down to the leaf state. Both API and annotation are supported to define history type of state. e.g. ```java @@ -516,6 +519,7 @@ To define the context event, user has two way, annotation or builder API. ``` **Note:** Before 0.3.7, user need to define "HistoryType.DEEP" for each level of historical state, which is not quite convenient.(Thanks to [Voskuijlen](https://github.com/Voskuijlen) to provide solution [Issue33](https://github.com/hekailiang/squirrel/issues/33)). Now user only define "HistoryType.DEEP" at the top level of historical state, and all its children state historical information will be remembered. + **Note:** "HistoryType.RECURSIVE" is not UML compliant and adds more control on the depth of the history. * **Transition Types** diff --git a/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/HistoryType.java b/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/HistoryType.java index 51cd47cf..4a06b3a3 100644 --- a/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/HistoryType.java +++ b/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/HistoryType.java @@ -19,6 +19,12 @@ public enum HistoryType { */ SHALLOW, + /** + * The state enters into its last active sub-state. The sub-state itself enters its last active sub-state if it has + * recursive history too and so on until the sub-state has no recursive history or the innermost nested state is reached. + */ + RECURSIVE, + /** * The state enters into its last active sub-state. The sub-state itself enters into-its last active state and so on until the innermost * nested state is reached. diff --git a/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/ImmutableState.java b/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/ImmutableState.java index 5b85c42e..af6e8e18 100644 --- a/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/ImmutableState.java +++ b/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/ImmutableState.java @@ -83,11 +83,20 @@ public interface ImmutableState, S, E, C> ex */ ImmutableState enterDeep(StateContext stateContext); + /** + * Enters this state is recursive mode: The entry action is executed and the + * last activate state is entered if it is in recursive mode itself, + * the initial state is entered otherwise. + * @param stateContext + * @return child state entered by recursive + */ + ImmutableState enterRecursive(StateContext stateContext); + /** * Enters this state is shallow mode: The entry action is executed and the * initial state is entered in shallow mode if there is one. * @param stateContext - * @return child state entered by shadow + * @return child state entered by shallow */ ImmutableState enterShallow(StateContext stateContext); diff --git a/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/impl/DotVisitorImpl.java b/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/impl/DotVisitorImpl.java index c04af768..1c89e7e2 100644 --- a/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/impl/DotVisitorImpl.java +++ b/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/impl/DotVisitorImpl.java @@ -31,10 +31,8 @@ public void visitOnEntry(ImmutableState visitable) { String stateLabel = visitable.getStateId().toString(); if (visitable.hasChildStates()) { writeLine("subgraph cluster_" + stateId + " {\nlabel=\"" + stateLabel + "\";"); - if (visitable.getHistoryType() == HistoryType.DEEP) { - writeLine(stateId + "History" + " [label=\"\"];"); - } else if (visitable.getHistoryType() == HistoryType.SHALLOW) { - writeLine(stateId + "History" + " [label=\"\"];"); + if (visitable.getHistoryType() != HistoryType.NONE) { + writeLine(stateId + "History" + " [label=\""+visitable.getHistoryType()+"\"];"); } } else { writeLine(stateId + " [label=\"" + stateLabel + "\"];"); diff --git a/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/impl/StateImpl.java b/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/impl/StateImpl.java index a91d98ce..383b7f0d 100644 --- a/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/impl/StateImpl.java +++ b/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/impl/StateImpl.java @@ -250,6 +250,9 @@ public ImmutableState enterByHistory(StateContext stateC case SHALLOW: result = enterHistoryShallow(stateContext); break; + case RECURSIVE: + result = enterHistoryRecursive(stateContext); + break; case DEEP: result = enterHistoryDeep(stateContext); break; @@ -266,6 +269,18 @@ public ImmutableState enterDeep(StateContext stateContex return lastActiveState == null ? this : lastActiveState.enterDeep(stateContext); } + @Override + public ImmutableState enterRecursive(StateContext stateContext) { + entry(stateContext); + if (this.getHistoryType() == HistoryType.RECURSIVE) { + final ImmutableState lastActiveState = + getLastActiveChildStateOf(this, stateContext.getStateMachineData().read()); + return lastActiveState == null ? this : lastActiveState.enterRecursive(stateContext); + } else { + return childInitialState!=null ? childInitialState.enterShallow(stateContext) : this; + } + } + @Override public ImmutableState enterShallow(StateContext stateContext) { entry(stateContext); @@ -284,6 +299,18 @@ private ImmutableState enterHistoryShallow(StateContext return lastActiveState != null ? lastActiveState.enterShallow(stateContext) : this; } + /** + * Enters this instance with history type = recursive. + * + * @param stateContext + * state context + * @return the entered state + */ + private ImmutableState enterHistoryRecursive(StateContext stateContext) { + final ImmutableState lastActiveState = getLastActiveChildStateOf(this, stateContext.getStateMachineData().read()); + return lastActiveState != null ? lastActiveState.enterRecursive(stateContext) : this; + } + /** * Enters with history type = none. * diff --git a/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/impl/StateMachineImporterImpl.java b/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/impl/StateMachineImporterImpl.java index b49839bc..6e54b93f 100644 --- a/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/impl/StateMachineImporterImpl.java +++ b/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/impl/StateMachineImporterImpl.java @@ -168,6 +168,8 @@ public void startElement(String uri, String localName, String qName, getCurrentState().setHistoryType(HistoryType.DEEP); } else if(historyType.equals("shallow")) { getCurrentState().setHistoryType(HistoryType.SHALLOW); + } else if(historyType.equals("recursive")) { + getCurrentState().setHistoryType(HistoryType.RECURSIVE); } } else if(qName.equals("onentry")) { isEntryAction = Boolean.TRUE; diff --git a/squirrel-foundation/src/test/java/org/squirrelframework/foundation/fsm/samples/RecursiveHistorySample.java b/squirrel-foundation/src/test/java/org/squirrelframework/foundation/fsm/samples/RecursiveHistorySample.java new file mode 100644 index 00000000..ade8a840 --- /dev/null +++ b/squirrel-foundation/src/test/java/org/squirrelframework/foundation/fsm/samples/RecursiveHistorySample.java @@ -0,0 +1,83 @@ +package org.squirrelframework.foundation.fsm.samples; + +import org.squirrelframework.foundation.fsm.annotation.State; +import org.squirrelframework.foundation.fsm.annotation.States; +import org.squirrelframework.foundation.fsm.annotation.Transit; +import org.squirrelframework.foundation.fsm.annotation.Transitions; +import org.squirrelframework.foundation.fsm.impl.AbstractStateMachine; +import org.squirrelframework.foundation.fsm.StateMachineBuilderFactory; +import org.squirrelframework.foundation.fsm.HistoryType; +import org.squirrelframework.foundation.fsm.StateMachineBuilder; + +enum SampleStates { + L1_A, L1_B, L1_C, L2_A, L2_B, L2_C, L31_A, L31_B, L32_A, L32_B +} + +enum SampleEvents { + E1, E2, E3, E4, E5, E6, E7, E8 +} + +class SampleContext { +} + +@States({ + @State(name = "L1_A", initialState = true), + @State(name = "L1_B", historyType = HistoryType.RECURSIVE), + @State(name = "L1_C"), + @State(name = "L2_A", parent = "L1_B", initialState = true), + @State(name = "L2_B", parent = "L1_B", historyType = HistoryType.RECURSIVE), + @State(name = "L2_C", parent = "L1_B"), + @State(name = "L31_A", parent = "L2_B", initialState = true), + @State(name = "L31_B", parent = "L2_B"), + @State(name = "L32_A", parent = "L2_C", initialState = true), + @State(name = "L32_B", parent = "L2_C"), +}) +@Transitions({ + @Transit(from = "L1_A", to = "L1_B", on = "E1"), + @Transit(from = "L2_A", to = "L2_B", on = "E2"), + @Transit(from = "L2_A", to = "L2_C", on = "E3"), + @Transit(from = "L31_A", to = "L31_B", on = "E4"), + @Transit(from = "L32_A", to = "L32_B", on = "E5"), + @Transit(from = "L1_B", to = "L1_C", on = "E6"), + @Transit(from = "L1_C", to = "L1_B", on = "E7"), + @Transit(from = "L1_C", to = "L2_A", on = "E8"), +}) +public class RecursiveHistorySample extends + AbstractStateMachine { + + public static RecursiveHistorySample create() { + StateMachineBuilder builder = StateMachineBuilderFactory + .create(RecursiveHistorySample.class, SampleStates.class, SampleEvents.class, SampleContext.class); + + return builder.newStateMachine(SampleStates.L1_A); + } + + public static void main(String[] args) { + final SampleContext sampleContext = new SampleContext(); + + RecursiveHistorySample sampleController = RecursiveHistorySample.create(); + + // Demonstrate recursive history through two levels + sampleController.fire(SampleEvents.E1); + sampleController.fire(SampleEvents.E2); + sampleController.fire(SampleEvents.E4); + System.out.println("Current state should be L31_B: " + sampleController.getCurrentState()); + sampleController.fire(SampleEvents.E6); + System.out.println("Current state should be L1_C : " + sampleController.getCurrentState()); + sampleController.fire(SampleEvents.E7); + System.out.println("Current state should be L31_B: " + sampleController.getCurrentState()); + + // Start back, do not use history + sampleController.fire(SampleEvents.E6); + sampleController.fire(SampleEvents.E8); + + // Demonstrate recursive history through one level only, using initial state on second level + sampleController.fire(SampleEvents.E3); + sampleController.fire(SampleEvents.E5); + System.out.println("Current state should be L32_B: " + sampleController.getCurrentState()); + sampleController.fire(SampleEvents.E6); + System.out.println("Current state should be L1_C : " + sampleController.getCurrentState()); + sampleController.fire(SampleEvents.E7); + System.out.println("Current state should be L32_A: " + sampleController.getCurrentState()); + } +} \ No newline at end of file From 3e47a5544d4ae432df54d93bc477406e0cccca7f Mon Sep 17 00:00:00 2001 From: Pierre-Louis MELIN Date: Tue, 4 Jun 2024 11:18:04 +0200 Subject: [PATCH 2/2] feat: added method 'processImmediate' to synchronously process one event and get the result There was no possibility to process synchronously one event in particular. the method 'canAccept' could be used to check if a transition would be accepted, but there was no guarantee that after the check the state machine was still in the same state. So this new method lets the user process the event synchronously and get the result of the processing; was the event accepted or not (no transition or rejected by guards). --- .../foundation/fsm/StateMachine.java | 19 ++++++++++++++ .../fsm/impl/AbstractStateMachine.java | 21 +++++++++++++++ .../fsm/samples/RecursiveHistorySample.java | 26 ++++++++++++++++--- .../fsm/threadsafe/AsyncExectionTest.java | 9 ++++--- 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/StateMachine.java b/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/StateMachine.java index 9d9072ad..de762425 100644 --- a/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/StateMachine.java +++ b/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/StateMachine.java @@ -37,6 +37,16 @@ public interface StateMachine, S, E, C> exten */ void fireImmediate(E event, C context); + /** + * Immediately processes the given event. If current state machine is busy, + * this call blocks until it is available for event processing. + * @param event the event + * @param context external context + * @return true if the event has been accepted, + * false otherwise (because no valid transition or because it was rejected by transition guards) + */ + boolean processImmediate(E event, C context); + /** * Test transition result under circumstance * @param event test event @@ -57,6 +67,15 @@ public interface StateMachine, S, E, C> exten */ void fireImmediate(E event); + /** + * Immediately processes the given event. If current state machine is busy, + * this call blocks until it is available for event processing. + * @param event the event + * @return true if the event has been accepted, + * false otherwise (because no valid transition or because it was rejected by transition guards) + */ + boolean processImmediate(E event); + /** * Test event * @param event diff --git a/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/impl/AbstractStateMachine.java b/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/impl/AbstractStateMachine.java index 0b4059d8..2628788b 100644 --- a/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/impl/AbstractStateMachine.java +++ b/squirrel-foundation/src/main/java/org/squirrelframework/foundation/fsm/impl/AbstractStateMachine.java @@ -293,6 +293,22 @@ public void fireImmediate(E event, C context) { fire(event, context, true); } + @Override + public boolean processImmediate(E event, C context) { + boolean eventAccepted = false; + writeLock.lock(); + setStatus(StateMachineStatus.BUSY); + eventAccepted = processEvent(event, context, data, executor, isDataIsolateEnabled); + ImmutableState rawState = data.read().currentRawState(); + if(isAutoTerminateEnabled && rawState.isRootState() && rawState.isFinalState()) { + terminate(context); + } + if(getStatus()==StateMachineStatus.BUSY) + setStatus(StateMachineStatus.IDLE); + writeLock.unlock(); + return eventAccepted; + } + @Override public void fire(E event) { fire(event, null); @@ -303,6 +319,11 @@ public void fireImmediate(E event) { fireImmediate(event, null); } + @Override + public boolean processImmediate(E event) { + return processImmediate(event, null); + } + /** * Clean all queued events */ diff --git a/squirrel-foundation/src/test/java/org/squirrelframework/foundation/fsm/samples/RecursiveHistorySample.java b/squirrel-foundation/src/test/java/org/squirrelframework/foundation/fsm/samples/RecursiveHistorySample.java index ade8a840..1cd266bc 100644 --- a/squirrel-foundation/src/test/java/org/squirrelframework/foundation/fsm/samples/RecursiveHistorySample.java +++ b/squirrel-foundation/src/test/java/org/squirrelframework/foundation/fsm/samples/RecursiveHistorySample.java @@ -6,6 +6,7 @@ import org.squirrelframework.foundation.fsm.annotation.Transitions; import org.squirrelframework.foundation.fsm.impl.AbstractStateMachine; import org.squirrelframework.foundation.fsm.StateMachineBuilderFactory; +import org.squirrelframework.foundation.fsm.AnonymousCondition; import org.squirrelframework.foundation.fsm.HistoryType; import org.squirrelframework.foundation.fsm.StateMachineBuilder; @@ -14,10 +15,18 @@ enum SampleStates { } enum SampleEvents { - E1, E2, E3, E4, E5, E6, E7, E8 + E1, E2, E3, E4, E5, E6, E7, E8, E9 } class SampleContext { + public boolean isOK; +} + +class G1 extends AnonymousCondition { + @Override + public boolean isSatisfied(SampleContext context) { + return context.isOK; + } } @States({ @@ -41,6 +50,7 @@ class SampleContext { @Transit(from = "L1_B", to = "L1_C", on = "E6"), @Transit(from = "L1_C", to = "L1_B", on = "E7"), @Transit(from = "L1_C", to = "L2_A", on = "E8"), + @Transit(from = "L1_A", to = "L1_C", on = "E9", when = G1.class), }) public class RecursiveHistorySample extends AbstractStateMachine { @@ -56,9 +66,18 @@ public static void main(String[] args) { final SampleContext sampleContext = new SampleContext(); RecursiveHistorySample sampleController = RecursiveHistorySample.create(); + sampleController.start(); + + sampleContext.isOK = false; + System.out.println("Should not be accepted (false): " + + sampleController.processImmediate(SampleEvents.E9, sampleContext)); + sampleContext.isOK = true; + System.out.println( + "Should be accepted (true): " + sampleController.processImmediate(SampleEvents.E9, sampleContext)); + System.out.println("Current state should be L1_C : " + sampleController.getCurrentState()); // Demonstrate recursive history through two levels - sampleController.fire(SampleEvents.E1); + sampleController.fire(SampleEvents.E8); sampleController.fire(SampleEvents.E2); sampleController.fire(SampleEvents.E4); System.out.println("Current state should be L31_B: " + sampleController.getCurrentState()); @@ -71,7 +90,8 @@ public static void main(String[] args) { sampleController.fire(SampleEvents.E6); sampleController.fire(SampleEvents.E8); - // Demonstrate recursive history through one level only, using initial state on second level + // Demonstrate recursive history through one level only, using initial state on + // second level sampleController.fire(SampleEvents.E3); sampleController.fire(SampleEvents.E5); System.out.println("Current state should be L32_B: " + sampleController.getCurrentState()); diff --git a/squirrel-foundation/src/test/java/org/squirrelframework/foundation/fsm/threadsafe/AsyncExectionTest.java b/squirrel-foundation/src/test/java/org/squirrelframework/foundation/fsm/threadsafe/AsyncExectionTest.java index c14aad6e..c332894e 100644 --- a/squirrel-foundation/src/test/java/org/squirrelframework/foundation/fsm/threadsafe/AsyncExectionTest.java +++ b/squirrel-foundation/src/test/java/org/squirrelframework/foundation/fsm/threadsafe/AsyncExectionTest.java @@ -50,7 +50,7 @@ public void execute(Object from, Object to, Object event, if (logger.length() > 0) { logger.append('.'); } - logger.append("AToBOnFIRST"); + logger.append("AToAOnFIRST"); } }); builder.transition().from("A").to("B").on("SECOND"); @@ -63,12 +63,15 @@ public void onTransitionDeclined() { }); fsm.start(); try { - TimeUnit.MILLISECONDS.sleep(500); + // the first EVENT is delayed of 10ms, so we should wait at least 410ms, + // and at most 500ms to ensure that exactly 5 FIRST events are triggered by A. + // Don't forget to keep enough time to process SECOND event before a 6th FIRST event is triggered. + TimeUnit.MILLISECONDS.sleep(450); } catch (InterruptedException e) { } fsm.fire("SECOND"); fsm.terminate(); - assertEquals("AToBOnFIRST.AToBOnFIRST.AToBOnFIRST.AToBOnFIRST.AToBOnFIRST", logger.toString()); + assertEquals("AToAOnFIRST.AToAOnFIRST.AToAOnFIRST.AToAOnFIRST.AToAOnFIRST", logger.toString()); } @Test