Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

GenStateMachine

genstatemachine.livemd

GenStateMachine

Setup

Mix.install([
  {:gen_state_machine, "~> 3.0"}
])

Event Timeout and State Timeout

The following example will start a GenStateMachine that behaves like a ticking clock which:

  1. Transition to the started event inmendiatly the same way handle_continue will behave on GenServers
  2. Schedule 2 timeouts. One to send a stop event after 2 seconds if there is no state change (state_timeout) and a event timeout to tick after 1 second (event_timeout)
  3. The tick event will arrive first and cancel the stop state_timeout by start ticking and transition to ticking state
  4. Also it will add again a state_timeout after 10 seconds to stop while schedule a tick event every second that will increase the count and continue scheduling a tick event as a timeout
  5. Lastly since the state didin’t change it will receive the stop event from the state_timeout
defmodule Ticker do
  use GenStateMachine, restart: :temporary

  require Logger

  def start_link(_) do
    GenStateMachine.start_link(__MODULE__, {:initial, %{count: 0}}, id: Ticker)
  end

  def init({state, data}) do
    Logger.info("Init")
    {:ok, state, data, [{:next_event, :internal, :start}]}
  end

  def handle_event(:internal, :start, :initial, data) do
    Logger.info("Start")

    {
      :next_state,
      :started,
      data,
      [
        {:state_timeout, 2_000, :stop},
        {:timeout, 1_000, :tick}
      ]
    }
  end

  def handle_event(:timeout, :tick, :started, data) do
    Logger.info("Now ticking")

    {
      :next_state,
      :ticking,
      increment(data),
      [
        {:state_timeout, 5_000, :stop},
        {:timeout, 1_000, :tick}
      ]
    }
  end

  def handle_event(:timeout, :tick, :ticking, data) do
    Logger.info("Tick #{data.count}")

    {
      :keep_state,
      increment(data),
      [{:timeout, 1_000, :tick}]
    }
  end

  def handle_event({:call, from}, :get_count, :ticking, data) do
    {:keep_state_and_data, [{:reply, from, data.count}]}
  end

  def handle_event(:state_timeout, :stop, _state, _data) do
    Logger.info("Stop")
    Process.exit(self(), :normal)
  end

  defp increment(data) do
    update_in(data, [:count], &(&1 + 1))
  end
end

# {:ok, pid} = Ticker.start_link([])
# Process.send_after(pid, :stop, 15_000)
# pid

Generic Timeout

General Timeout is triggered independenly of events or state. The following example:

  1. Initialise the GenStateMachine and sets 2 timeouts: a GenericTimeout to kill the process after 15 seconds, an event timeout to tick every second
  2. The GenStateMachine starts ticking every second
  3. A call event is triggered to get the count which will interrupt the ticking
  4. After a while the process is kill with the initial timeout
defmodule Ticker2 do
  use GenStateMachine, restart: :temporary

  require Logger

  def start_link(_) do
    GenStateMachine.start_link(__MODULE__, {:initial, %{count: 0}}, id: Ticker2)
  end

  def init({state, data}) do
    Logger.info("Init")
    {:ok, state, data, [{:next_event, :internal, :start}]}
  end

  def handle_event(:internal, :start, :initial, data) do
    Logger.info("Start")

    {
      :next_state,
      :started,
      data,
      [
        {{:timeout, :kill}, 10_000, :stop},
        {:timeout, 1_000, :tick}
      ]
    }
  end

  def handle_event(:timeout, :tick, :started, data) do
    Logger.info("Tick #{data.count}")

    {
      :keep_state,
      increment(data),
      {:timeout, 1_000, :tick}
    }
  end

  def handle_event(:info, :get_count, _state, data) do
    Logger.info("The count is #{data.count}")
    :keep_state_and_data
  end

  def handle_event(:cast, :increment, :started, data) do
    {:keep_state, increment(data)}
  end

  def handle_event({:call, from}, :get_count, _state, data) do
    {:keep_state_and_data, [{:reply, from, data.count}]}
  end

  def handle_event({:call, from}, :stop, _state, data) do
    Logger.info("Stopping")
    {:next_state, :stopping, data, [{:reply, from, :ok}, {:next_event, :internal, :stop}]}
  end

  def handle_event({:timeout, :kill}, :stop, _state, _data) do
    Logger.info("Killing")
    Process.exit(self(), :normal)
  end

  def handle_event(:internal, :stop, :stopping, _data) do
    Logger.info("Stop!")
    {:stop, :normal}
  end

  defp increment(data) do
    update_in(data, [:count], &(&1 + 1))
  end
end

{:ok, pid} = Ticker2.start_link([])

# Process.send_after(pid, :get_count, 2_000)
Process.sleep(3_000)
# GenStateMachine.call(pid, :get_count)
GenStateMachine.call(pid, :stop)