View Source Finitomata behaviour (Finitomata v0.22.0)
Bird View
Finitomata
provides a boilerplate for FSM implementation, allowing to concentrate on the business logic rather than on the process management and transitions/events consistency tweaking.
It reads a description of the FSM from a string in PlantUML, Mermaid, or even custom format.
Syntax Definition
Mermaid
state diagram format is literally the same asPlantUML
, so if you want to use it, specifysyntax: :state_diagram
and if you want to use mermaid graph, specifysyntax: :flowchart
. The latter is the default.
Basically, it looks more or less like this
PlantUML
/ :state_diagram
[*] --> s1 : to_s1
s1 --> s2 : to_s2
s1 --> s3 : to_s3
s2 --> [*] : ok
s3 --> [*] : ok
Mermaid
/ :flowchart
s1 --> |to_s2| s2
s1 --> |to_s3| s3
Using
syntax: :flowchart
Mermaid
does not allow to explicitly specify transitions (and hence event names) from the starting state and to the end state(s), these states names are implicitly set to:*
and events to:__start__
and:__end__
respectively.
Finitomata
validates the FSM is consistent, namely it has a single initial state, one or more final states, and no orphan states. If everything is OK, it generates a GenServer
that could be used both alone, and with provided supervision tree. This GenServer
requires to implement six callbacks
on_transition/4
— mandatoryon_failure/3
— optionalon_enter/2
— optionalon_exit/2
— optionalon_terminate/1
— optionalon_timer/2
— optional
All the callbacks do have a default implementation, that would perfectly handle transitions having a single to state and not requiring any additional business logic attached.
Upon start, it moves to the next to initial state and sits there awaiting for the transition request. Then it would call an on_transition/4
callback and move to the next state, or remain in the current one, according to the response.
Upon reaching a final state, it would terminate itself. The process keeps all the history of states it went through, and might have a payload in its state.
Special Events
If the event name is ended with a bang (e. g. idle --> |start!| started
) and
this event is the only one allowed from this state (there might be several transitions though,)
it’d be considered as determined and FSM will be transitioned into the new state instantly.
If the event name is ended with a question mark (e. g. idle --> |start?| started
,)
the transition is considered as expected to fail; no on_failure/2
callback would
be called on failure and no log warning will be printed.
FSM Tuning and Configuration
Recurrent Callback
If timer: non_neg_integer()
option is passed to use Finitomata
,
then Finitomata.on_timer/2
callback will be executed recurrently.
This might be helpful if FSM needs to update its state from the outside
world on regular basis.
Automatic FSM Termination
If auto_terminate: true() | state() | [state()]
option is passed to use Finitomata
,
the special __end__
event to transition to the end state will be called automatically
under the hood, if the current state is either listed explicitly, or if the value of
the parameter is true
.
Ensuring State Entry
If ensure_entry: true() | [state()]
option is passed to use Finitomata
, the transition
attempt will be retried with {:continue, {:transition, {event(), event_payload()}}}
message
until succeeded. Neither on_failure/2
callback is called nor warning message is logged.
The payload would be updated to hold __retries__: pos_integer()
key. If the payload was not a map,
it will be converted to a map %{payload: payload}
.
Example
Let’s define the FSM instance
defmodule MyFSM do
@fsm """
s1 --> |to_s2| s2
s1 --> |to_s3| s3
"""
use Finitomata, fsm: @fsm, syntax: :flowchart
## or uncomment lines below for `:state_diagram` syntax
# @fsm """
# [*] --> s1 : to_s1
# s1 --> s2 : to_s2
# s1 --> s3 : to_s3
# s2 --> [*] : __end__
# s3 --> [*] : __end__
# """
# use Finitomata, fsm: @fsm, syntax: :state_diagram
@impl Finitomata
def on_transition(:s1, :to_s2, _event_payload, state_payload),
do: {:ok, :s2, state_payload}
end
Now we can play with it a bit.
# or embed into supervision tree using `Finitomata.child_spec()`
{:ok, _pid} = Finitomata.start_link()
Finitomata.start_fsm MyFSM, "My first FSM", %{foo: :bar}
Finitomata.transition "My first FSM", {:to_s2, nil}
Finitomata.state "My first FSM"
#⇒ %Finitomata.State{current: :s2, history: [:s1], payload: %{foo: :bar}}
Finitomata.allowed? "My first FSM", :* # state
#⇒ true
Finitomata.responds? "My first FSM", :to_s2 # event
#⇒ false
Finitomata.transition "My first FSM", {:__end__, nil} # to final state
#⇒ [info] [◉ ⇄] [state: %Finitomata.State{current: :s2, history: [:s1], payload: %{foo: :bar}}]
Finitomata.alive? "My first FSM"
#⇒ false
Typically, one would implement all the on_transition/4
handlers, pattern matching on the state/event.
use Finitomata
When you
use Finitomata
, the Finitomata module will do the following things for your module:
- set
@behaviour Finitomata
- compile and validate FSM declaration, passed as
fsm:
keyword argument- turn the module into
GenServer
- inject default implementations of optional callbacks specified with
impl_for:
keyword argument (default::all
)- expose a bunch of functions to query FSM which would be visible in docs
- leaves
on_transition/4
mandatory callback to be implemeneted by the calling module and injectsbefore_compile
callback to validate the implementation (this option required:finitomata
to be included in the list of compilers inmix.exs
)
Options to use Finitomata
:fsm
(String.t/0
) - Required. The FSM declaration with the syntax defined bysyntax
option.:syntax
- The FSM dialect parser to convert the declaration to internal FSM representation. The default value is:flowchart
.:impl_for
- The list of transitions to inject default implementation for. The default value is:all
.:timer
(pos_integer/0
) - The interval to callon_timer/2
recurrent event. The default value is5000
.:auto_terminate
- Whentrue
, the transition to the end state is initiated automatically. The default value isfalse
.:ensure_entry
- The list of states to retry transition to until succeeded. The default value is[]
.:shutdown
(pos_integer/0
) - The shutdown interval for theGenServer
behind the FSM. The default value is5000
.:persistency
- The implementation ofFinitomata.Persistency
behaviour to backup FSM with a persistent storage. The default value isnil
.:listener
- The implementation ofFinitomata.Listener
behaviour or aGenServer.name()
to receive notification after transitions. The default value isnil
.
Summary
Types
The payload that can be passed to each call to transition/3
The name of the FSM (might be any term, but it must be unique)
The ID of the Finitomata
supervision tree, useful for the concurrent
using of different Finitomata
supervision trees.
The resolution of transition, when {:error, _}
tuple, the transition is aborted
Error types of FSM validation
Callbacks
This callback will be called on entering the state.
This callback will be called on exiting the state.
This callback will be called if the transition failed to complete to allow the consumer to take an action upon failure.
This callback will be called from the underlying GenServer.init/1
.
This callback will be called on transition to the final state to allow the consumer to perform some cleanup, or like.
This callback will be called recurrently if timer: pos_integer()
option has been given to use Finitomata
.
This callback will be called from each transition processor.
Functions
Returns true
if the FSM specified is alive, false
otherwise.
Returns true
if the transition to the state state
is possible, false
otherwise.
Fully qualified name of the FSM backed by Finitonata
Fast check to validate the FSM process with such id
and target
exists.
Returns true
if the transition by the event event
is possible, false
otherwise.
Starts the FSM instance.
The state of the FSM.
Returns true
if the supervision tree is alive, false
otherwise.
Returns supervision tree of Finitomata
. The healthy tree has all three pid
s.
Initiates the transition.
Types
@type event_payload() :: any()
The payload that can be passed to each call to transition/3
@type fsm_name() :: any()
The name of the FSM (might be any term, but it must be unique)
@type id() :: any()
The ID of the Finitomata
supervision tree, useful for the concurrent
using of different Finitomata
supervision trees.
@type transition_resolution() :: {:ok, Finitomata.Transition.state(), Finitomata.State.payload()} | {:error, any()}
The resolution of transition, when {:error, _}
tuple, the transition is aborted
@type validation_error() ::
:initial_state | :final_state | :orphan_from_state | :orphan_to_state
Error types of FSM validation
Callbacks
@callback on_enter( current_state :: Finitomata.Transition.state(), state :: Finitomata.State.t() ) :: :ok
This callback will be called on entering the state.
@callback on_exit( current_state :: Finitomata.Transition.state(), state :: Finitomata.State.t() ) :: :ok
This callback will be called on exiting the state.
@callback on_failure( event :: Finitomata.Transition.event(), event_payload :: event_payload(), state :: Finitomata.State.t() ) :: :ok
This callback will be called if the transition failed to complete to allow the consumer to take an action upon failure.
@callback on_start(state :: Finitomata.State.payload()) :: {:continue, Finitomata.State.payload()} | {:ok, Finitomata.State.payload()} | :ignore
This callback will be called from the underlying GenServer.init/1
.
Unlike other callbacks, this one might raise preventing the whole FSM from start.
When :ignore
, or {:continues, new_payload}
tuple is returned from the callback,
the normal initalization continues through continuing to the next state.
{:ok, new_payload}
prevents the FSM from automatically getting into start state,
and the respective transition must be called manually.
@callback on_terminate(state :: Finitomata.State.t()) :: :ok
This callback will be called on transition to the final state to allow the consumer to perform some cleanup, or like.
@callback on_timer( current_state :: Finitomata.Transition.state(), state :: Finitomata.State.t() ) :: :ok | {:ok, Finitomata.State.payload()} | {:transition, {Finitomata.Transition.event(), event_payload()}, Finitomata.State.payload()} | {:transition, Finitomata.Transition.event(), Finitomata.State.payload()} | {:reschedule, non_neg_integer()}
This callback will be called recurrently if timer: pos_integer()
option has been given to use Finitomata
.
@callback on_transition( current_state :: Finitomata.Transition.state(), event :: Finitomata.Transition.event(), event_payload :: event_payload(), state_payload :: Finitomata.State.payload() ) :: transition_resolution()
This callback will be called from each transition processor.
Functions
Returns true
if the FSM specified is alive, false
otherwise.
@spec allowed?(id(), fsm_name(), Finitomata.Transition.state()) :: boolean()
Returns true
if the transition to the state state
is possible, false
otherwise.
Fully qualified name of the FSM backed by Finitonata
Fast check to validate the FSM process with such id
and target
exists.
The arguments are
- the id of the FSM (optional)
- the name of the FSM
@spec responds?(id(), fsm_name(), Finitomata.Transition.event()) :: boolean()
Returns true
if the transition by the event event
is possible, false
otherwise.
@spec start_fsm(id(), module(), any(), any()) :: DynamicSupervisor.on_start_child()
Starts the FSM instance.
The arguments are
- the global name of
Finitomata
instance (optional, defaults toFinitomata
) - the name of the FSM (might be any term, but it must be unique)
- the implementation of FSM (the module, having
use Finitomata
) - the payload to be carried in the FSM state during the lifecycle
Before v0.15.0
the second and third parameters were expected in different order.
This is deprecated and will be removed in v1.0.0
.
The FSM is started supervised. If the global name/id is given, it should be passed
to all calls like transition/4
@spec state(id(), fsm_name(), reload? :: :cached | :payload | :full) :: nil | Finitomata.State.t() | Finitomata.State.payload()
The state of the FSM.
The arguments are
- the id of the FSM (optional)
- the name of the FSM
- defines whether the cached state might be returned or should be reloaded
Returns true
if the supervision tree is alive, false
otherwise.
Returns supervision tree of Finitomata
. The healthy tree has all three pid
s.
@spec transition( id(), fsm_name(), Finitomata.Transition.event() | {Finitomata.Transition.event(), Finitomata.State.payload()}, non_neg_integer() ) :: :ok
Initiates the transition.
The arguments are
- the id of the FSM (optional)
- the name of the FSM
event
atom or{event, event_payload}
tuple; the payload will be passed to the respectiveon_transition/4
call, payload isnil
by defaultdelay
(optional) the interval in milliseconds to apply transition after