StateEngine.md 18 KB

Wayne.Lib.StateEngine

This document describes the functionality and ideas behind the State engine library. To see a tutorial of StateEngine see Tutorial

Introduction

The state engine design is based on the theories of FSM (Finite State Machine). It is a model of an object that has an ability to be in different states, and depending on its state it reacts differently to events from surrounding systems. The change between states is called Transitions.

There are different interpretations of the FSM theory, and the naming varies. In this introduction, the theory basis for the state engine class library will be described.

There are some state kinds.

State

This is an ordinary state where the object can be. It can make transitions to other states with a transition. A state has three possibilities to react to events from the surrounding systems: Enter - executes when the state is entered Exit - executes when the state is left HandleEvent- executes when something is sent from a surrounding system. The state machine determines, depending on the current state, which action to take based on the event. UML Notiation: A rounded rectangle

PseudoState

A pseudo state is a state that not really is a state, because the object cannot be in this state, it must directly make a transition to another state. UML notation: Can be a rhomb if it is a choice state.

InitialState

Initial state is a start point of a state machine. The initial state is a special case of the Pseudo state, and therefore state machine cannot stay in this state. It must transition directly to another state. A state machine must contain an Initial state; otherwise it won't know where to begin.

UML Notation: A filled circle.

CompositeState

A composite state is a state that has substates. When the object is in this state, its behaviour is determined by not only what state it is in, but also what state the state is in. In fact, there is a complete new state machine contained in the composite state, and a separate state chart can be drawn for the inner state machine.

When entering a composite state we can specify three different history types. History type none - default. When entering the composite state, it always starts at its initial state. History type shallow - The composite state returns to the sub state it was in when it was last left. UML Notation: A circle with an H in:
History type deep - The composite state will, like in the shallow history type, return to the state it was in when it last exited, but if that state is another composite state, it will enter its substate too as far as it goes. UML notaion: A circle with an H and an asterisk

FinalState

The final state marks the endpoint of a state machine. It is mostly used within composite states to mark the end of a processing. The state machine cannot transition from the final state. UML Notiation: A circle containing another, filled, circle.

StateEngine class library

The StateEngine library is an implementation of the theories described in the introduction. The basis for the design is the following

  • Each state is represented by an object.
  • Input to the state machine is event objects of the class StateEngineEvent.
  • An event is interpreted by the current state, which sends a Transition object to the State machine.
  • The state machine keeps a lookup table that finds the next state based on the transition type.

State class

When implementing code for a state, the StateEngine.State class is inherited. It has three methods that can be overridden:

• Enter • Exit • HandleEvent

It is optional to call the base version of these methods. The state class is the base class for all other state classes.

PseudoState class

The PseudoState class is used when the state machine cannot stay in this state, but must transition directly. This is used when we want to interpret something in the environment to determine which transition to proceed with. We cannot use Enter, Exit or HandleEvent in PseudoState. Instead the method CreatePseudoStateTransition is mandatory to override, since it is an abstract method.

InitialState class

The Initial state is a pseudo state that actually not has any new functionality from the base class. There should be one and only one initial state in a state machine, and it is automatically entered when starting up the machine.

CompositeState class

Composite state is a special state that owns a separate state machine that represents a substate of the state. It is possible to override Enter, Exit, but not HandleEvent. It is used internally. Instead, override the methods BeforeHandleEvent and UnhandledEvent. The statemachine that is contained must be set up in the same way as any state machine (see statemachine description)

FinalState class

Final state represent an endpoint in a state machine. There can not be any transitions from this state to any other state in the same state machine. There can be several final states in one state machine. When the Final state has been reached, and after the Entry, the state engine will automatically post a BasicTransitionType.Done transition to the state machine above the state machine containing the final state. By overriding the Enter method of a final state, the default transition can be overridden with a custom transition.

StateMachine class

The state machine class is the engine of the state machine, and owns the states. It must also be associated with one or more state factories that can produce the state objects. Normally it should not be necessary to derive from the StateMachine class, it should be enough to instantiate and configure it.

A statemachine object can only be created through the static factory method StateMachine.Create(). Here the user specifies the implementation that is required. There are two implementations for the StateMachine class that executes differently.

Threaded State machine

The threaded state machine has its own thread, where all state machine code executes. All execution in any state machine is triggered by an event. When the event is delivered to the state machine by some other thread, it is queued and processed by the state machine thread.

Synchronized state machine

This implementation does not contain any thread of its own. Instead it is executing in the thread that delivered the event. If one thread is already is executing when another thread delivers an event, the event is just queued. The event will then be handled by the thread that was running. So the impact for threads to deliver events to a state machine is less predictable than in the threaded state machine. The upside of using the synchronous state machine is that it consumes less system resources, since it does not have to maintain a separate thread.

Event handling

When an event is sent into the state machine it will first be queued for handling. The state machine will then pick out the event from the queue and send it to the current state. If the current state does not mark the event as handled, the event will be put on a resend queue. When the state changes, all events in the resend queue will be moved to the normal incoming event queue. This is a prioritized queue where the event’s priority and the arrival time will determine the handling order. This makes it absolutely necessary to set the Handled flag in the event objects when they are handled, otherwise they will live forever in the state machine.

Features

This section describes features that have been implemented in the state engine library to help the developer create simple and intuitive applications.

Timer

A state can create and activate timers that sends timer events after a specified time.

public override void Enter(StateEngine.StateEntry Entry)
{
     this.ActivateTimer(new StateEngine.Timer(this, MicroEvents.Timer, 20000);
}

This row will activate a timer that will send a TimerEvent with the type MicroEvents.Timer to the HandleEvent method of the state. Per default, the timer will be disabled when the state is left, but it can also be configured that it is still active after the state is left, and the timer event will be sent to the state that is active when the timeout occurs. The timers will also per default only fire once, and then remove itself, but a periodic timer can also be configured.

Built-in transitions

The state engine will post a few built-in transitions in different situations.

BasicTransitionType.Init

The init transition is posted to the Initial state in the state machine. It is of no use to add handlers for the Init transitions, since it will only be used in that situation. Of course can the type be used for other transitions as well, but it is not recommended.

BasicTransitionType.Done

A CompositeState state machine that is transitioned into a FinalState will automatically post a Done transition in the parent state machine.

BasicTransitionType.Error

When an error occurs in the state machine the Error transition is posted. The application use this to go to error states. The state engine sends this transition when an exception has slipped out of the user-functions Enter or HandleEvent. If we get an exception in Exit, it is logged, but does not send any automatic transition.

AnyState as source state in transition setup.

In many state machines we want a state that can be entered from all states in a simple way. A good example of this is the Error transition described above. Therefore we can implement a wildcard transition that will go to a specified state whatever state we are in. It is entered in the state transition setup as the dummy state StateEngine.AnyState.

lookup.AddTransition<Wayne.Lib.StateEngine.AnyState, ErrorState>( BasicTransitionType.Error)

If another state-transition setup is configured for the same transition, it will override the wildcard transition.

ExplicitTransition

In some cases, we want to perform transitions to a specific state and short-circuit the state-transition configuration, and go directly to a known state. This cannot be used as a replacement to the state-transition configuration, since the configuration is also used to determine what state objects to create.

One example where to use explicit transition is when we are transitioned to an Error state with a general transition from AnyState described above. In some cases we want to transition back to where we came from.

When we enter the state we get a StateEntry object. This contains a reference to the source transition. That in turn has a reference to the source state. We save a reference to the source state in the error-state object. When we want to leave the error state, we create an ExplicitTransition to the state we came from.

Example


class ErrorState : StateEngine.State
{
    StateEngine.State cameFromState;

    public override void Enter(StateEngine.StateEntry Entry, ref  Wayne.Lib.StateEngine.Transition transition)
    {
        base.Enter(Entry); 
        cameFromState = Entry.SourceTransition.Sender; // save reference to sourcestate 

        //Show a popup or something. When it is closed it sends event type 
// MicroEvents.ErrorConfirmation
        //
    }

    public override void HandleEvent(StateEngine.StateEngineEvent stateEngineEvent, 
      ref Wayne.Lib.StateEngine.Transition transition)
    {
        base.HandleEvent(stateEngineEvent);
        if (stateEngineEvent.Type == MicroEvents.ErrorConfirmation)
        {
            transition=new StateEngine.ExplicitTransition(this,	//Sender                                                        
MicroTransitions.Done,        //Transition type
                                      cameFromState.Name);        	//Target state
        }
    }
}

The target state is looked up through the state name. The state is searched in the following order:

  1. Search in the same state machine as this state
  2. Search in all sub-state machines for this state machine.
  3. Go to the parent state machine to this, and search there.

Generic event

Normally you need to add more data to an event than the event type. You can create a subclass of the StateEngineEvent class and add the additional data. If you get the data from an EventArgs object, typically in a .Net event, the GenericEvent class can be used to encapsulate this data without needing to create a new class.


void SomeEventFired(object sender, EventArgs e)
{
    stateMachine.IncomingEvent(new GenericEvent<EventArgs>(States.EventType.EventHappened, sender, e));
}

This approach is a bridge between the .Net event handling pattern and the StateEngine event handling.

The code can be further simplified by using the static method GenericEvent.Create(), that detects the type of the EventArgs by inferred usage.

void SomeEventFired(object sender, EventArgs e)
{
    stateMachine.IncomingEvent(GenericEvent.Create(States.EventType.EventHappened, sender, e));
}

Generic states

In all cases the state objects in a state machine needs to reference an object that it represents the logic for. In the example with the Microwave oven above, in each of the state classes there is a reference to the IMicrowaveOven object. The generic states are a set of state classes that has expanded the functionality of the base state classes with the possibility to have a Main object already in the state. The type of the Main object is specified as a generic parameter to the class (Wayne.Lib.StateEngine.Generic.State). This means that you always have a property named “Main” that is of the type IMicrowaveOven. In addition to the normal state classes, there are two additional base state classes that is made for the convenience of the developer:

  • TimeoutState – A state class with a built-in timeout behavior
  • AsyncWorkState – A state class that spawns a thread from the thread pool. When the thread execution finishes, it posts a transition automatically.
  • Generic states with StateData

    In a statemachine, it is normally needed to store data on the main object, in the case of the microwave oven, the storage possibilities consists of the get and set properties in the IMicrowaveOven interface. This creates model where all data is accessible from all states, i.e. the access does not have a scope. The lifetime of the data must also be managed by explicit code. The Generic states that supports StateData contains functionality to create scopes for the data. Each generic state specifies, in addition to the Main object type, the type of the state data. Operative.Composite Where IMicrowaveOven is the Main object type and OperativeStateData defines what data that can be accessed from this state. The Main object is still accessed via the Main property. The state data object is accessed from the Data property. Each state data is created by a composite state. The lowermost level of the state machine can not have state data, it needs to use the Main object as storage point. Example We have a state machine that is built up like:

    
    Inoperative<MyMainObject>
    Operative.Composite<MyMainObject, OperativeStateData>
    	Init<MyMainObject, OperativeStateData>
    Idle<MyMainObject,OperativeStateData>
    PerformATask.Composite <MyMainObject, PerformATaskOperativeData>
    
    	Init<MyMainObject, PerformATaskStateData >
    	DoSomeStuff<MyMainObject, PerformATaskStateData>
    	Final<MyMainObject, PerformATaskStateData >
    
    MajorError<MyMainObject>
    

    OperativeStateData is a class that inherits from the StateEngine’s StateData class and is normally stored in the source tree in the same project folder as the composite state it belongs to.

    
    class OperativeStateData : StateData
    {
    	public int A {get;set;} 
    }
    

    PerformATaskStateData is inheriting from OperativeStateData, since the PerformATask state wants its own data, but also to be able to access the data that is defined for the parent composite state Operative.

    class PerformATaskStateData : OperativeStateData
    {
    	Public int B {get;set}
    }
    

    This means that from Operative.Idle we have access to Data.A. In PerformATask.DoSomeStuff we have access to Data.A and Data.B. To make this work, the StateData classes can not store the data locally in the object but have to use the facilities of the StateData class (DefineData, GetDate, SetData) to store the data so the lifetime of the different data gets correct. In order to implement a state data class like OperativeStateData, the following steps needs to be made:

    1. Create a class that inherits from class Wayne.Lib.StateEngine.Generic.StateData class OperativeStateData : StateData Define an enumeration for the data that is used inside this statedata class. It is good if it’s a nested type, since it’s not going to be used anywhere else.

      private enum Defines
      {
      A
      } 
      
    2. Define the parameter with it’s type in the constructor of OperativeStateData. The constructor must have the parameter signature with at least the parameter StateData parentStateData that is passed to the base class.

      public OperativeStateData (StateData parentStateData):base(parentStateData)
      {
      	DefineData(Defines.A, (int)0);
      }
      
    3. Create the property that the state machine code uses to access the data.

      public int A
      {
      get { return GetData<Defines, int>(Defines.A); }
      set { SetData(Defines.A, value); }
      }
      

    When adding more properties, they follow the same pattern, to be defined, and then get/set code like the A property.

    Description attributes

    The description attributes is used to document a state machine and its logic. There are tools (Waynel.Lib.StateEngine.Analyzer) to analyze a state machine based on the description attributes combined with the transition tables. It is good practice to apply the description attributes to all state machines correctly. The description attributes can also be extracted to create a document that shows the state machine logic.

    • StateDescription – Gives a overall description of the state and its purpose.
    • EnterDescription – Describes what conditions that might trigger a direct transition from Enter.
    • EventDescription – Describes which events that are handled and if those triggers transitions.
    • KeywordDescription – Optional description of different keywords associated with the state.
    • ImageDescription – A way to define an image that can be automatically inserted to the statemachine documentation document.