Skip to content

Finite State Machine Model

Constantine edited this page Nov 7, 2020 · 23 revisions

This page describes the FSM model


The Finite State Machine

A finite state machine in Tron is defined by a single interface that declares the union of all transitions in the FSM. This interface is referred to as the Transitions interface for the FSM. These methods all have the return type of the same Transitions interface where the methods are defined. For example:

public interface MyFsm {
   MyFsm transitionA();
   MyFsm transitionB();
   MyFsm transitionC();
}

Note that transitions are simply methods; they may take arguments:

public interface Protocol {
   Protocol readReady(ByteBuffer in);
   Protocol writeReady(ByteBuffer out);
   Protocol accept(SocketChannel channel);
}

Finite State Machine Context

Every FSM has a context object that implements the actions of the FSM. This object is supplied when the FSM instance is created. To access this in the FSM implementation, two methods are available. The first is to call the static accessor Fsm.thisFsm() and Fsm.thisContext(). Alternatively, as shown in these examples, your FSM transitions interface can extend the FsmExecutor interface. This interface packages up access to the Fsm instance and context, making the code far cleaner. For example:

public interface MyFsm extends FsmExecutor<MyContext, MyFsm> {
   MyFsm transitionA();
   MyFsm transitionB();
   MyFsm transitionC();
}

With this in place, one can now use this.fsm() and this.context() for access. This mechanism is used throughout the examples below.

FSM Execution State

The executing state of an FSM is composed of:

  • current state - the state the Fsm is currently in.
  • previous state - the previous state of the Fsm
  • transition - the last transition fired on the previous state
  • state stack - the stack of pushed states
  • log - the SLF4J logger that the instance uses

Synchronization

Tron provides thread safe running of FSM instances. This ensures that state transitions within the FSM happen synchronously. When constructing an FSM, the code can choose to enable synchronization or leave it ravaged by your multi threaded whims, you stateless, asynchronous brute you.

State Model

States are represented in Tron by Java enum singleton instances. The enclosing enumeration class of these singletons forms a natural grouping of states, forming a nice analog to the SMC state map. The Java enumeration class implements the Fsm Transitions interface:

public enum State implements MyFsm {
    A, B, C, D;

   public MyFsm transitionA() { return null; }
   public MyFsm transitionB() { return null; }
   public MyFsm transitionC() { return null; }
}

In the example above, the methods of the MyFsm interface are defined on the enclosing Java enumeration class. Java enumerations also allow defining methods on the enumeration singleton instances themselves. Thus, we can specialize transition behavior on the state enum, rather than simply for the entire set of enumeration values. For example:

public enum State implements MyFsm {
    A() {
       public MyFsm transitionA() { return B; }
    }, 
    B() {
       public MyFsm transitionA() { return A; }
       public MyFsm transitionB() { return C; }
    }, 
    C() {
       public MyFsm transitionC() { return D; }
    }, 
    D() {
       public MyFsm transitionA() { return A; }
    };

   public MyFsm transitionA() { return null; }
   public MyFsm transitionB() { return null; }
   public MyFsm transitionC() { return null; }
}

In this example, we have four states: A, B, C and D. State A defines transitionA(). State B defines transtitions transitionA() and transitonB(). States C and D define the transitions transitionC() and transitionA() respectively.

Transition

As mentioned previously, transitions are simply methods on a Java enumeration singleton. A transition method may have arguments and returns the next state of the Fsm.

Simple Transition

Simple Transition

A simple transition is a method that returns the next state of the FSM. In its simplest form:

Idle() {
    public MyFsm run() { return Running }

In this example, the next state of the FSM when this run() transition is fired is the Running state.

An External Loopback Transition

An External Loopback Transition

An external loopback leaves the current state of the FSM the same and "loops" back to it. This means that the state's exit and entry actions are executed. This is in contrast to an internal loopback transition. For example:

Idle() {
    public MyFsm timeout() { return Idle }
}

An Internal Loopback Transition

An Internal Loopback Transition

Returning null as the next state causes the Fsm to remain in the current state. This means that the state's exit and entry actions are not executed. This is in contrast to an external loopback transition. For example:

Idle() {
    public MyFsm timeout() { return null }
}

A Transition with Actions

A Transition with Actions

As transitions are methods, any arbitrary code can be executed in them. For example:

Idle() {
    public MyFsm run() {
        context().stopTimer("Idle");
        context().doWork();
        return Running;
    }
}

FSM Execution State When Actions are Performed

All actions execute with a constant FSM execution state, regardless of the type of transaction. For example, if firing a push or pop transaction, the current state of the FSM, during the entire execution of the transition method remains unchanged.

Note that this is different from the SMC semantics, in that actions taken in the transition - i.e. actions in the transition method - execute with the current state of the FSM set. In SMC, the current state of the FSM is undefined.

Transition Guards

Transition Guards

Transition guards are simple the code used to implement the decision about how to transition to the next state. For example:

Idle() {
    public MyFsm run() {
        MyContext ctxt = context();
        // Guard condition
        if (ctxt.isValid()) {
            // Actions
            ctxt.stopTimer("Idle");
            ctxt.doWork();
            // Next State
            return Running
        }
        ctxt.rejectRequest();
        return null; // loopback transition
    }
}

Push Transition

Push Transition

For example:

Running() {
    public MyFsm blocked() {
        fsm().push(Wait.Blocked);
        context().getResource();
        return BlockPop;
    }
}

This causes the state machine to:

  1. Execute all actions in the transition in the current state of the Fsm.
  2. Transition to the BlockPop state.
  3. Push to the Wait.Blocked state.
  4. Execute the Wait.Blocked entry actions.

When Fsm issues a pop transition, control will return to BlockPop state and the pop transition is issued with that as the current state.

Pop Transition

Pop Transition

The pop transition differs from the simple and push transition in that:

The end state is not specified. That is because the pop transition will return to whatever state issued the corresponding push. The state returned by the transition is ignored.

The pop returns the return state to which the Fsm will pop as the result of calling the pop() method of the Fsm. You can use the return value to send a transition to the popped state.

In the above example, if the resource request is granted, the state machine returns to the corresponding state that did the push and then takes that state's OK transition. If the request is denied, the same thing happens except the FAILED transition is taken. For example:

Waiting() {
    public MyFsm granted() {
        fsm().pop().ok();
        context().cleanUp();
        return null; // value always ignored in a pop transition
    }
    public MyFsm denied() {
        fsm().pop().failed();
        context().cleanUp();
        return null; // value always ignored in a pop transition
    }
}

Default Transitions

What happens if a state receives a transition that is not defined in that state? Tron has two separate mechanisms for handling that situation: the default state and the default transition of a state.

Default State

The "default" state is the outer enum class of the state enum singletons. The class must, as it implements the Transitions interface, defines all the transitions of the interface. These are the methods that serve as the transitions of the "default" state. State enum singletons, of course, may override this default transition behavior with their own method definitions.

Default Transition

Any state enum may define a "default" transition for the state. The default transition is a no argument method returning the Transitions interface. It is marked with @Default:

Waiting() {
    @Default
    public MyFsm defaultTransition() {
         context().protocolError();
    }
}

Invalid Transitions

If a transition is not defined for a given state, the implementation of the transition should throw the InvalidTransition exception:

Waiting() { 
    public MyFsm waiting() {
         throw new InvalidTransition();
    }
}

Note that because all transition methods must be defined on the enclosing enum class, you only need to define the invalid transition once, on this outer class, rather than implementing all transitions methods on the enumeration singleton instance:

public enum Protocol {
    Waiting;
    public MyFsm waiting() {
        throw new InvalidTransition();
    }
}

Transition Lookup Order

The addition of two kinds of default transitions means that Tron does not simply invoke the first method it finds. The Tron FSM looks up transitions on the enumeration singleton in the following order:

  1. Declared methods on the state enumeration singleton
  2. Methods inherited by the state enumeration from the enclosing enum class

If the transition method throws an InvalidTransition exception, then:

  1. Declared method annotated with @Default on the state enumeration singleton
  2. Method annotated with @Default on the enclosing enum class

Note that Putting a @Default transition in the enclosing enum class means that all transitions will be handled - it is the transition method of last resort.

Note that you should always throw the InvalidTransition exception if the state does not use the transition. This can be done in the "default" transitions of the enclosing enumeration class, so you don't have to implement these on each singleton - the "default" transition will be inherited by the enum singletons. Throwing the InvalidTransition is essential to the lookup process for the @Default transition, defined either in the enum singleton or the enclosing enumeration class.

Entry and Exit Actions

Entry and Exit Actions

When a transition leaves a state, it executes the state's exit actions after executing the transition actions. When a transition enters a state, it executes the state's entry actions. A transition executes actions in this order:

  1. The transition actions (code in the method)
  2. "From" state's exit actions.
  3. Set the current state to the "to" state.
  4. "To" state's entry actions.

Entry and Exit actions are void methods on the state enumeration that take no arguments. They are marked with the @Entry and @Exit annotation. For example:

Login() {
    @Entry
    public void entry() {
        context().dispatchLogin();
    }
}