Search documentation of Finitomata

# View Source Finitomata behaviour (Finitomata v0.21.2)

## 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 as `PlantUML`, so if you want to use it, specify `syntax: :state_diagram` and if you want to use mermaid graph, specify `syntax: :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` — mandatory
- `on_failure/3` — optional
- `on_enter/2` — optional
- `on_exit/2` — optional
- `on_terminate/1` — optional
- `on_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 injects `before_compile` callback to validate the implementation (this option required `:finitomata` to be included in the list of compilers in `mix.exs`)

## Options to `use Finitomata`

- `:fsm` (`String.t/0`) - Required. The FSM declaration with the syntax defined by `syntax` 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 call `on_timer/2` recurrent event. The default value is `5000`.

- `:auto_terminate` - When `true`, the transition to the end state is initiated automatically. The default value is `false`.

- `:ensure_entry` - The list of states to retry transition to until succeeded. The default value is `[]`.

- `:shutdown` (`pos_integer/0`) - The shutdown interval for the `GenServer` behind the FSM. The default value is `5000`.

- `:persistency` - The implementation of `Finitomata.Persistency` behaviour to backup FSM with a persistent storage. The default value is `nil`.

- `:listener` - The implementation of `Finitomata.Listener` behaviour or a `GenServer.name()` to receive notification after transitions. The default value is `nil`.

# Summary

## Types

event_payload()

The payload that can be passed to each call to `transition/3`

fsm_name()

The name of the FSM (might be any term, but it must be unique)

id()

The ID of the `Finitomata` supervision tree, useful for the concurrent using of different `Finitomata` supervision trees.

transition_resolution()

The resolution of transition, when `{:error, _}` tuple, the transition is aborted

validation_error()

Error types of FSM validation

## Callbacks

on_enter(current_state, state)

This callback will be called on entering the state.

on_exit(current_state, state)

This callback will be called on exiting the state.

on_failure(event, event_payload, state)

This callback will be called if the transition failed to complete to allow the consumer to take an action upon failure.

on_start(state)

This callback will be called from the underlying `GenServer.init/1`.

on_terminate(state)

This callback will be called on transition to the final state to allow the consumer to perform some cleanup, or like.

on_timer(current_state, state)

This callback will be called recurrently if `timer: pos_integer()` option has been given to `use Finitomata`.

on_transition(current_state, event, event_payload, state_payload)

This callback will be called from each transition processor.

## Functions

alive?(id \\ nil, target)

Returns `true` if the FSM specified is alive, `false` otherwise.

allowed?(id \\ nil, target, state)

Returns `true` if the transition to the state `state` is possible, `false` otherwise.

fqn(id, name)

Fully qualified name of the FSM backed by `Finitonata`

lookup(id \\ nil, target)

Fast check to validate the FSM process with such `id` and `target` exists.

responds?(id \\ nil, target, event)

Returns `true` if the transition by the event `event` is possible, `false` otherwise.

start_fsm(id \\ nil, impl, name, payload)

Starts the FSM instance.

state(id \\ nil, target, reload? \\ :full)

The state of the FSM.

sup_alive?(id \\ nil)

Returns `true` if the supervision tree is alive, `false` otherwise.

sup_tree(id \\ nil)

Returns supervision tree of `Finitomata`. The healthy tree has all three `pid`s.

transition(id \\ nil, target, event_payload, delay \\ 0)

Initiates the transition.

# Types

Link to this type

# event_payload()

View Source

    @type event_payload() :: any()

The payload that can be passed to each call to `transition/3`

Link to this type

# fsm_name()

View Source

    @type fsm_name() :: any()

The name of the FSM (might be any term, but it must be unique)

Link to this type

# id()

View Source

    @type id() :: any()

The ID of the `Finitomata` supervision tree, useful for the concurrent using of different `Finitomata` supervision trees.

Link to this type

# transition_resolution()

View Source

    @type transition_resolution() ::
      {:ok, Finitomata.Transition.state(), Finitomata.State.payload()}
      | {:error, any()}

The resolution of transition, when `{:error, _}` tuple, the transition is aborted

Link to this type

# validation_error()

View Source

    @type validation_error() ::
      :initial_state | :final_state | :orphan_from_state | :orphan_to_state

Error types of FSM validation

# Callbacks

Link to this callback

# on_enter(current_state, state)

View Source (optional)

    @callback on_enter(
      current_state :: Finitomata.Transition.state(),
      state :: Finitomata.State.t()
    ) :: :ok

This callback will be called on entering the state.

Link to this callback

# on_exit(current_state, state)

View Source (optional)

    @callback on_exit(
      current_state :: Finitomata.Transition.state(),
      state :: Finitomata.State.t()
    ) :: :ok

This callback will be called on exiting the state.

Link to this callback

# on_failure(event, event_payload, state)

View Source (optional)

    @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.

Link to this callback

# on_start(state)

View Source (optional)

    @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.

Link to this callback

# on_terminate(state)

View Source (optional)

    @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.

Link to this callback

# on_timer(current_state, state)

View Source (optional)

    @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`.

Link to this callback

# on_transition(current_state, event, event_payload, state_payload)

View Source

    @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

Link to this function

# alive?(id \\ nil, target)

View Source

    @spec alive?(any(), fsm_name()) :: boolean()

Returns `true` if the FSM specified is alive, `false` otherwise.

Link to this function

# allowed?(id \\ nil, target, state)

View Source

    @spec allowed?(id(), fsm_name(), Finitomata.Transition.state()) :: boolean()

Returns `true` if the transition to the state `state` is possible, `false` otherwise.

Link to this function

# fqn(id, name)

View Source

    @spec fqn(any(), fsm_name()) :: {:via, module(), {module(), any()}}

Fully qualified name of the FSM backed by `Finitonata`

Link to this function

# lookup(id \\ nil, target)

View Source

    @spec lookup(id(), fsm_name()) :: pid() | nil

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

Link to this function

# responds?(id \\ nil, target, event)

View Source

    @spec responds?(id(), fsm_name(), Finitomata.Transition.event()) :: boolean()

Returns `true` if the transition by the event `event` is possible, `false` otherwise.

Link to this function

# start_fsm(id \\ nil, impl, name, payload)

View Source

    @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 to `Finitomata`)
- 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`

Link to this function

# state(id \\ nil, target, reload? \\ :full)

View Source

    @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

Link to this function

# sup_alive?(id \\ nil)

View Source

    @spec sup_alive?(id()) :: boolean()

Returns `true` if the supervision tree is alive, `false` otherwise.

Link to this function

# sup_tree(id \\ nil)

View Source

    @spec sup_tree(id()) :: [
      supervisor: nil | pid(),
      manager: nil | pid(),
      registry: nil | pid()
    ]

Returns supervision tree of `Finitomata`. The healthy tree has all three `pid`s.

Link to this function

# transition(id \\ nil, target, event_payload, delay \\ 0)

View Source

    @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 respective `on_transition/4` call, payload is `nil` by default
- `delay` (optional) the interval in milliseconds to apply transition after