GenStateMachine behaviour (gen_state_machine v3.0.0) View Source
A behaviour module for implementing a state machine.
The advantage of using this module is that it will have a standard set of interface functions and include functionality for tracing and error reporting. It will also fit into a supervision tree.
Example
The GenStateMachine
behaviour abstracts the state machine. Developers are only
required to implement the callbacks and functionality they are interested in.
Let's start with a code example and then explore the available callbacks.
Imagine we want a GenStateMachine
that works like a switch, allowing us to
turn it on and off, as well as see how many times the switch has been turned
on:
defmodule Switch do
use GenStateMachine
# Callbacks
def handle_event(:cast, :flip, :off, data) do
{:next_state, :on, data + 1}
end
def handle_event(:cast, :flip, :on, data) do
{:next_state, :off, data}
end
def handle_event({:call, from}, :get_count, state, data) do
{:next_state, state, data, [{:reply, from, data}]}
end
end
# Start the server
{:ok, pid} = GenStateMachine.start_link(Switch, {:off, 0})
# This is the client
GenStateMachine.cast(pid, :flip)
#=> :ok
GenStateMachine.call(pid, :get_count)
#=> 1
We start our Switch
by calling start_link/3
, passing the module with the
server implementation and its initial argument, a tuple where the first element
is the initial state and the second is the initial data. We can primarily
interact with the state machine by sending two types of messages. call
messages expect a reply from the server (and are therefore synchronous) while
cast messages do not.
Every time you do a call/3
or a cast/2
, the message will be handled by
handle_event/4
.
We can also use the :state_functions
callback mode instead of the default,
which is :handle_event_function
:
defmodule Switch do
use GenStateMachine, callback_mode: :state_functions
def off(:cast, :flip, data) do
{:next_state, :on, data + 1}
end
def off({:call, from}, :get_count, data) do
{:keep_state_and_data, [{:reply, from, data}]}
end
def on(:cast, :flip, data) do
{:next_state, :off, data}
end
def on({:call, from}, :get_count, data) do
{:keep_state_and_data, [{:reply, from, data}]}
end
end
# Start the server
{:ok, pid} = GenStateMachine.start_link(Switch, {:off, 0})
# This is the client
GenStateMachine.cast(pid, :flip)
#=> :ok
GenStateMachine.call(pid, :get_count)
#=> 1
Again, we start our Switch
by calling start_link/3
, passing the module
with the server implementation and its initial argument, and interacting with
it via call and cast.
However, in :state_functions
callback mode, every time you do a call/3
or
a cast/2
, the message will be handled by the state_name/3
function which
is named the same as the current state.
Callbacks
In the default :handle_event_function
callback mode, there are 4 callbacks
required to be implemented. By adding use GenStateMachine
to your module,
Elixir will automatically define all 4 callbacks for you, leaving it up to you
to implement the ones you want to customize.
In the :state_functions
callback mode, there are 3 callbacks required to be
implemented. By adding use GenStateMachine, callback_mode: :state_functions
to your module, Elixir will automatically define all 3 callbacks for you,
leaving it up to you to implement the ones you want to customize, as well as
state_name/3
functions named the same as the states you wish to support.
It is important to note that the default implementation of the code_change/4
callback results in an :undefined
error. This is because code_change/4
is
related to the quite difficult topic of hot upgrades, and if you need it, you
should really be implementing it yourself. In normal use this callback will
not be invoked.
State Enter Callbacks
If you wish, you can register to receive callbacks when you enter a state by
adding :state_enter
to your callback_mode
:
defmodule Switch do
use GenStateMachine, callback_mode: [:handle_event_function, :state_enter]
def handle_event(:enter, _event, state, data) do
{:next_state, state, %{data | enters: data.enters + 1}}
end
def handle_event(:cast, :flip, :off, data) do
{:next_state, :on, %{data | flips: data.flips + 1}}
end
def handle_event(:cast, :flip, :on, data) do
{:next_state, :off, data}
end
def handle_event({:call, from}, :get_count, _state, data) do
{:keep_state_and_data, [{:reply, from, data}]}
end
end
# Start the server
{:ok, pid} = GenStateMachine.start_link(Switch, {:off, %{enters: 0, flips: 0}})
# This is the client
GenStateMachine.cast(pid, :flip)
GenStateMachine.cast(pid, :flip)
#=> :ok
GenStateMachine.call(pid, :get_count)
#=> %{enters: 3, flips: 1}
See the Erlang documentation for more details.
Name Registration
Both start_link/3
and start/3
support registering the GenStateMachine
under a given name on start via the :name
option. Registered names are also
automatically cleaned up on termination. The supported values are:
an atom - the
GenStateMachine
is registered locally with the given name usingProcess.register/2
.{:global, term}
- theGenStateMachine
is registered globally with the given term using the functions in the:global
module.{:via, module, term}
- theGenStateMachine
is registered with the given mechanism and name. The:via
option expects a module that exportsregister_name/2
,unregister_name/1
,whereis_name/1
andsend/2
. One such example is the:global
module which uses these functions for keeping the list of names of processes and their associated pid's that are available globally for a network of Erlang nodes.
For example, we could start and register our Switch server locally as follows:
# Start the server and register it locally with name MySwitch
{:ok, _} = GenStateMachine.start_link(Switch, {:off, 0}, name: MySwitch)
# Now messages can be sent directly to MySwitch
GenStateMachine.call(MySwitch, :get_count) #=> 0
Once the server is started, the remaining functions in this module (call/3
,
cast/2
, and friends) will also accept an atom, or any :global
or :via
tuples. In general, the following formats are supported:
- a
pid
- an
atom
if the server is locally registered {atom, node}
if the server is locally registered at another node{:global, term}
if the server is globally registered{:via, module, name}
if the server is registered through an alternative registry
Client / Server APIs
Although in the example above we have used GenStateMachine.start_link/3
and
friends to directly start and communicate with the server, most of the
time we don't call the GenStateMachine
functions directly. Instead, we wrap
the calls in new functions representing the public API of the server.
Here is a better implementation of our Switch module:
defmodule Switch do
use GenStateMachine
# Client
def start_link() do
GenStateMachine.start_link(Switch, {:off, 0})
end
def flip(pid) do
GenStateMachine.cast(pid, :flip)
end
def get_count(pid) do
GenStateMachine.call(pid, :get_count)
end
# Server (callbacks)
def handle_event(:cast, :flip, :off, data) do
{:next_state, :on, data + 1}
end
def handle_event(:cast, :flip, :on, data) do
{:next_state, :off, data}
end
def handle_event({:call, from}, :get_count, state, data) do
{:next_state, state, data, [{:reply, from, data}]}
end
def handle_event(event_type, event_content, state, data) do
# Call the default implementation from GenStateMachine
super(event_type, event_content, state, data)
end
end
In practice, it is common to have both server and client functions in the same module. If the server and/or client implementations are growing complex, you may want to have them in different modules.
Receiving custom messages
The goal of a GenStateMachine
is to abstract the "receive" loop for
developers, automatically handling system messages, support code change,
synchronous calls and more. Therefore, you should never call your own
"receive" inside the GenStateMachine
callbacks as doing so will cause the
GenStateMachine
to misbehave. If you want to receive custom messages, they
will be delivered to the usual handler for your callback mode with event_type
:info
.
Learn more
If you wish to find out more about gen_statem, the documentation and links in Erlang can provide extra insight.
Link to this section Summary
Types
State transition actions.
The callback mode for the GenStateMachine.
The persistent data (similar to a GenServer's state
) for the GenStateMachine.
The message content received as the result of an event.
The return type of an event handler function.
The source of the current event.
The return type when the server is started.
The type of allowed server names.
The term representing the current state.
The return type of a state enter function.
The atom representing the current state in :state_functions
callback mode.
Functions
Makes a synchronous call to the server
and waits for its reply.
Sends an asynchronous request to the server
.
Sends replies to clients.
Replies to a client.
Starts a GenStateMachine
process without links (outside of a supervision
tree).
Starts a GenStateMachine
process linked to the current process.
Stops the server with the given reason
.
Callbacks
Invoked to change the state of the GenStateMachine
when a different version
of a module is loaded (hot code swapping) and the state and/or data's term
structure should be changed.
Invoked in some cases to retrieve a formatted version of the GenStateMachine
status.
Whenever a GenStateMachine
in callback mode :handle_event_function
(the
default) receives a call, cast, or normal process messsage, this callback will
be invoked.
Invoked when the server is started. start_link/3
(or start/3
) will
block until it returns.
Whenever a GenStateMachine
in callback mode :state_functions
receives a
call, cast, or normal process message, a state function is called.
Invoked when the server is about to exit. It should do any cleanup required.
Link to this section Types
Specs
action() :: :gen_statem.action()
State transition actions.
They may be invoked by returning them from a state function or init/1.
If present in a list of actions, they are executed in order, and any that set transition options (postpone, hibernate, and timeout) override any previously provided options of the same type.
If the state changes, the queue of incoming events is reset to start with the oldest postponed.
All events added as a result of a :next_event
action are inserted in the
queue to be processed before all other events. An event of type :internal
should be used when you want to reliably distinguish an event inserted this
way from an external event.
See the Erlang documentation for the possible values.
Specs
callback_mode_result() :: :gen_statem.callback_mode_result()
The callback mode for the GenStateMachine.
See the Example section above for more info.
Specs
data() :: term()
The persistent data (similar to a GenServer's state
) for the GenStateMachine.
Specs
event_content() :: term()
The message content received as the result of an event.
Specs
event_handler_result(state) :: :gen_statem.event_handler_result(state)
The return type of an event handler function.
See the Erlang documentation for a complete reference.
Specs
event_type() :: :gen_statem.event_type()
The source of the current event.
{:call, from}
will be received as a result of a call.
:cast
will be received as a result of a cast.
:info
will be received as a result of any regular process messages received.
:timeout
or {:timeout, name}
will be received as a result of a :timeout
action.
:state_timeout
will be received as a result of a :state_timeout
action.
:internal
will be received as a result of a :next_event
action.
See the Erlang documentation for details.
Specs
on_start() :: :gen_statem.start_ret()
The return type when the server is started.
See the Erlang documentation for a complete reference.
Specs
server_ref() :: :gen_statem.server_ref()
The type of allowed server names.
See the Erlang documentation for a complete reference.
Specs
state() :: state_name() | term()
The term representing the current state.
In :handle_event_function
callback mode, any term.
In :state_functions
callback mode, an atom.
Specs
state_enter_result(state) :: :gen_statem.state_enter_result(state)
The return type of a state enter function.
See the Erlang documentation for a complete reference.
Specs
state_name() :: atom()
The atom representing the current state in :state_functions
callback mode.
Link to this section Functions
Specs
call(server_ref(), term(), timeout()) :: term()
Makes a synchronous call to the server
and waits for its reply.
The client sends the given request
to the server and waits until a reply
arrives or a timeout occurs. The appropriate state function will be called on
the server to handle the request.
server
can be any of the values described in the "Name registration"
section of the documentation for this module.
Timeouts
timeout
is an integer greater than zero which specifies how many
milliseconds to wait for a reply, or the atom :infinity
to wait
indefinitely. The default value is :infinity
. If no reply is received within
the specified time, the function call fails and the caller exits.
If the caller catches an exit, to avoid getting a late reply in the caller's
inbox, this function spawns a proxy process that does the call. A late reply
gets delivered to the dead proxy process, and hence gets discarded. This is
less efficient than using :infinity
as a timeout.
Specs
cast(server_ref(), term()) :: :ok
Sends an asynchronous request to the server
.
This function always returns :ok
regardless of whether
the destination server
(or node) exists. Therefore it
is unknown whether the destination server
successfully
handled the message.
The appropriate state function will be called on the server to handle the request.
Specs
reply([:gen_statem.reply_action()] | :gen_statem.reply_action()) :: :ok
Sends replies to clients.
Can be used to explicitly send replies to multiple clients.
This function always returns :ok
.
See reply/2
for more information.
Specs
reply(:gen_statem.from(), term()) :: :ok
Replies to a client.
This function can be used to explicitly send a reply to a client that called
call/3
when the reply cannot be specified in the return value of a state
function.
client
must be the from
element of the event type accepted by state
functions. reply
is an arbitrary term which will be given
back to the client as the return value of the call.
Note that reply/2
can be called from any process, not just the one
that originally received the call (as long as that process communicated the
from
argument somehow).
This function always returns :ok
.
Examples
def handle_event({:call, from}, :reply_in_one_second, state, data) do
Process.send_after(self(), {:reply, from}, 1_000)
:keep_state_and_data
end
def handle_event(:info, {:reply, from}, state, data) do
GenStateMachine.reply(from, :one_second_has_passed)
end
Specs
start(module(), any(), GenServer.options()) :: on_start()
Starts a GenStateMachine
process without links (outside of a supervision
tree).
See start_link/3
for more information.
Specs
start_link(module(), any(), GenServer.options()) :: on_start()
Starts a GenStateMachine
process linked to the current process.
This is often used to start the GenStateMachine
as part of a supervision
tree.
Once the server is started, the init/1
function of the given module
is
called with args
as its arguments to initialize the server. To ensure a
synchronized start-up procedure, this function does not return until init/1
has returned.
Note that a GenStateMachine
started with start_link/3
is linked to the
parent process and will exit in case of crashes from the parent. The
GenStateMachine
will also exit due to the :normal
reasons in case it is
configured to trap exits in the init/1
callback.
Options
:name
- used for name registration as described in the "Name registration" section of the module documentation:timeout
- if present, the server is allowed to spend the given amount of milliseconds initializing or it will be terminated and the start function will return{:error, :timeout}
:debug
- if present, the corresponding function in the:sys
module is invoked:spawn_opt
- if present, its value is passed as options to the underlying process as inProcess.spawn/4
Return values
If the server is successfully created and initialized, this function returns
{:ok, pid}
, where pid
is the pid of the server. If a process with the
specified server name already exists, this function returns
{:error, {:already_started, pid}}
with the pid of that process.
If the init/1
callback fails with reason
, this function returns
{:error, reason}
. Otherwise, if it returns {:stop, reason}
or :ignore
, the process is terminated and this function returns
{:error, reason}
or :ignore
, respectively.
Specs
stop(server_ref(), reason :: term(), timeout()) :: :ok
Stops the server with the given reason
.
The terminate/2
callback of the given server
will be invoked before
exiting. This function returns :ok
if the server terminates with the
given reason; if it terminates with another reason, the call exits.
This function keeps OTP semantics regarding error reporting.
If the reason is any other than :normal
, :shutdown
or
{:shutdown, _}
, an error report is logged.
Link to this section Callbacks
Specs
code_change( old_vsn :: term() | {:down, vsn :: term()}, state(), data(), extra :: term() ) :: {:ok, state(), data()} | (reason :: term())
Invoked to change the state of the GenStateMachine
when a different version
of a module is loaded (hot code swapping) and the state and/or data's term
structure should be changed.
old_vsn
is the previous version of the module (defined by the @vsn
attribute) when upgrading. When downgrading the previous version is wrapped in
a 2-tuple with first element :down
. state
is the current state of the
GenStateMachine
, data
is the current data, and extra
is any extra data
required to change the state.
Returning {:ok, new_state, new_data}
changes the state to new_state
, the
data to new_data
, and the code change is successful.
Returning reason
fails the code change with reason reason
and the state
and data remains the same.
If code_change/4
raises the code change fails and the loop will continue
with its previous state. Therefore this callback does not usually contain side
effects.
This function can optionally throw a result to return it.
Specs
Invoked in some cases to retrieve a formatted version of the GenStateMachine
status.
This callback can be useful to control the appearance of the status of the
GenStateMachine
. For example, it can be used to return a compact
representation of the GenStateMachines
's state/data to avoid having large
terms printed.
one of
:sys.get_status/1
or:sys.get_status/2
is invoked to get the status of theGenStateMachine
; in such cases,reason
is:normal
the
GenStateMachine
terminates abnormally and logs an error; in such cases,reason
is:terminate
pdict_state_and_data
is a three-element list [pdict, state, data]
where
pdict
is a list of {key, value}
tuples representing the current process
dictionary of the GenStateMachine
, state
is the current state of the
GenStateMachine
, and data
is the current data.
This function can optionally throw a result to return it.
Specs
handle_event(:enter, old_state :: state(), state(), data()) :: state_enter_result(state())
handle_event(event_type(), event_content(), state(), data()) :: event_handler_result(state())
Whenever a GenStateMachine
in callback mode :handle_event_function
(the
default) receives a call, cast, or normal process messsage, this callback will
be invoked.
This function can optionally throw a result to return it.
See the Erlang documentation for a complete reference.
Specs
init(args :: term()) :: :gen_statem.init_result(state())
Invoked when the server is started. start_link/3
(or start/3
) will
block until it returns.
args
is the argument term (second argument) passed to start_link/3
.
Returning {:ok, state, data}
will cause start_link/3
to return
{:ok, pid}
and the process to enter its loop.
Returning {:ok, state, data, actions}
is similar to {:ok, state}
except the provided actions will be executed.
Returning :ignore
will cause start_link/3
to return :ignore
and the
process will exit normally without entering the loop or calling terminate/2
.
If used when part of a supervision tree the parent supervisor will not fail
to start nor immediately try to restart the GenStateMachine
. The remainder
of the supervision tree will be (re)started and so the GenStateMachine
should not be required by other processes. It can be started later with
Supervisor.restart_child/2
as the child specification is saved in the parent
supervisor. The main use cases for this are:
- The
GenStateMachine
is disabled by configuration but might be enabled later. - An error occurred and it will be handled by a different mechanism than the
Supervisor
. Likely this approach involves callingSupervisor.restart_child/2
after a delay to attempt a restart.
Returning {:stop, reason}
will cause start_link/3
to return
{:error, reason}
and the process to exit with reason reason
without
entering the loop or calling terminate/2
.
This function can optionally throw a result to return it.
Specs
state_name(:enter, old_state_name :: state_name(), data()) :: state_enter_result(state_name())
state_name(event_type(), event_content(), data()) :: event_handler_result(state_name())
Whenever a GenStateMachine
in callback mode :state_functions
receives a
call, cast, or normal process message, a state function is called.
This spec exists to document the callback, but in actual use the name of the
function is probably not going to be state_name
. Instead, there will be at
least one state function named after each state you wish to handle. See the
Examples section above for more info.
These functions can optionally throw a result to return it.
See the Erlang documentation for a complete reference.
Specs
Invoked when the server is about to exit. It should do any cleanup required.
reason
is exit reason, state
is the current state, and data
is the
current data of the GenStateMachine
. The return value is ignored.
terminate/2
is called if a callback (except init/1
) returns a :stop
tuple, raises, calls Kernel.exit/1
or returns an invalid value. It may also
be called if the GenStateMachine
traps exits using Process.flag/2
and
the parent process sends an exit signal.
If part of a supervision tree a GenStateMachine
's Supervisor
will send an
exit signal when shutting it down. The exit signal is based on the shutdown
strategy in the child's specification. If it is :brutal_kill
the
GenStateMachine
is killed and so terminate/2
is not called. However if it
is a timeout the Supervisor
will send the exit signal :shutdown
and the
GenStateMachine
will have the duration of the timeout to call terminate/2
- if the process is still alive after the timeout it is killed.
If the GenStateMachine
receives an exit signal (that is not :normal
) from
any process when it is not trapping exits it will exit abruptly with the same
reason and so not call terminate/2
. Note that a process does NOT trap
exits by default and an exit signal is sent when a linked process exits or its
node is disconnected.
Therefore it is not guaranteed that terminate/2
is called when a
GenStateMachine
exits. For such reasons, we usually recommend important
clean-up rules to happen in separated processes either by use of monitoring or
by links themselves. For example if the GenStateMachine
controls a port
(e.g. :gen_tcp.socket
) or File.io_device
, they will be closed on receiving
a GenStateMachine
's exit signal and do not need to be closed in
terminate/2
.
If reason
is not :normal
, :shutdown
nor {:shutdown, term}
an error is
logged.
This function can optionally throw a result, which is ignored.