Skip to content

FSM implementation generated from Mermaid/PlantUML textual representation

License

Notifications You must be signed in to change notification settings

am-kantox/finitomata

Repository files navigation

Finitomata Finitomata    Kantox ❤ OSS  Test  Dialyzer

The FSM boilerplate based on callbacks


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 {: .tip}

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 {: .tip}

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/4mandatory
  • 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 c: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}.

Examples

See examples directory for real-life examples of Finitomata usage.

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.


Installation

def deps do
  [
    {:finitomata, "~> 0.20"}
  ]
end

Changelog

  • 0.26.0 — [UPD] a lot of tiny improvements, Finitomata.Accessible, reset_timer message + tests, experimental Finitomata.Cache
  • 0.25.0 — [UPD] allow assertions of entry states in Finitomata.ExUnit
  • 0.24.2 — [UPD/FIX] many fixes for better diagnostics in Finitomata.ExUnit
  • 0.23.7 — [UPD] allow both :mox and {:mox, MyApp.Listener} as well as just MyApp.Listener as a listener in FSM definition
  • 0.23.4 — [FIX] many fixes to a Finitomata.ExUnit test scaffold generation
  • 0.23.0 — [UPD] mix finitomata.generate.test --module MyApp.FSM to generate a Finitomata.ExUnit test scaffold
  • 0.22.0 — [FIX] Infinitomata.start_fsm/4 is finally 102% sync
  • 0.21.4 — [FIX] Finitomata.Pool initialization in cluster
  • 0.21.3 — [FIX] proper return from Infinitomata.start_fsm/4
  • 0.21.1 — [UPD] listener: :mox and better Finitomata.ExUnit docs
  • 0.20.2 — [UPD] allow guard matches in the RHO of ~> operator in assert_transition/3
  • 0.20.0 — [FIX] starting pool on distribution, re-synch on :badrpc failure
  • 0.19.0 — [UPD] Finitomata.ExUnit lighten options check (compile-time module dependencies suck in >=1.16)
  • 0.18.0 — [UPD] asynchronous Finitomata.Pool on top of Infinitomata
  • 0.17.0 — [UPD] careful naming and Finitomata.Throttler
  • 0.16.0 — [UPD] Infinitomata as a self-contained distributed implementation leveraging :pg
  • 0.15.0 — [UPD] support snippet formatting for modern Elixir
  • 0.14.6 — [FIX] persistency flaw when loading [credits @peaceful-james]
  • 0.14.5 — [FIX] require Logger in Hook
  • 0.14.4 — [FIX] Docs cleanup (credits: @TwistingTwists), PlantUML proper entry
  • 0.14.3 — [FIX] Draw diagram in docs
  • 0.14.2 — [FIX] Stop Events process
  • 0.14.1 — [FIX] Incorrect detection of superfluous determined transitions
  • 0.14.0Finitomata.ExUnit improvements
  • 0.13.0 — compile-time helpers for FSM, Finitomata.ExUnit
  • 0.12.1c:Finitomata.on_start/1 callback
  • 0.11.3 — [FIX] better error message for options (credits @ray-sh)
  • 0.11.2 — [DEBT] exported Finitomata.fqn/2
  • 0.11.1Inspect, :flowchart/:state_diagram as default parsers, behaviour Parser
  • 0.11.0{:ok, state_payload} return from on_timer/2, :persistent_term to cache state
  • 0.10.0 — support for several supervision trees with ids, experimental support for persistence scaffold
  • 0.9.0 — [FIX] malformed callbacks had the FSM broken
  • 0.8.2 — last error is now kept in the state (credits to @egidijusz)
  • 0.8.1 — improvements to :finitomata compiler
  • 0.8.0:finitomata compiler to warn/hint about not implemented ambiguous transitions
  • 0.7.2 — [FIX] banged! transitions must not be determined
  • 0.6.3soft? events which do not call on_failure/2 and do not log errors
  • 0.6.2ensure_entry: option to retry a transition
  • 0.6.1 — code cleanup + auto_terminate: option to make :__end__ transition imminent
  • 0.6.0on_timer/2 and banged imminent transitions
  • 0.5.2state() type on generated FSMs
  • 0.5.1 — fixed specs [credits @egidijusz]
  • 0.5.0 — all callbacks but on_transition/4 are optional, accept impl_for: param to use Finitomata
  • 0.4.0 — allow anonymous FSM instances
  • 0.3.0en_entry/2 and on_exit/2 optional callbacks
  • 0.2.0Mermaid support

Documentation.