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:
-
Transition to the
startedevent inmendiatly the same wayhandle_continuewill behave on GenServers -
Schedule 2 timeouts. One to send a
stopevent after 2 seconds if there is no state change (state_timeout) and a event timeout totickafter 1 second (event_timeout) -
The
tickevent will arrive first and cancel the stopstate_timeoutby start ticking and transition totickingstate -
Also it will add again a
state_timeoutafter 10 seconds tostopwhile schedule atickevent every second that will increase the count and continue scheduling atickevent as a timeout -
Lastly since the state didin’t change it will receive the
stopevent from thestate_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:
- Initialise the GenStateMachine and sets 2 timeouts: a GenericTimeout to kill the process after 15 seconds, an event timeout to tick every second
- The GenStateMachine starts ticking every second
- A call event is triggered to get the count which will interrupt the ticking
- 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)