state_server v0.4.6 StateServer.State behaviour View Source
A behaviour that lets you organize code for your StateServer
states.
Organization
When you define your StateServer
, the StateServer
module gives you the
opportunity to define state modules. These are typically (but not
necessarily) submodules scoped under the main StateServer
module. In
this way, your code for handling events can be neatly organized by
state. In some (but not all) cases, this may be the most appropriate
way to keep your state machine codebase sane.
Defining the state module.
the basic syntax for defining a state module is as follows:
defstate MyModule, for: :state do
# ... code goes here ...
def handle_call(:increment, _from, data) do
{:reply, :ok, update: data + 1}
end
end
note that the callback directives defined in this module are identical
to those of StateServer
, except that they are missing the state
argument.
External state modules
You might want to use an external module to handle event processing for one your state machine. Reasons might include:
- to enable code reuse between state machines
- if your codebase is getting too long and you would like to put state modules in different files.
If you choose to do so, there is a short form defstate
call, which is
as follows:
defstate ExternalModule, for: :state
Be sure to mark your ExternalModule
as having the StateServer.State
behaviour.
Precedence and Defer statements
Note that handle_*
functions written directly in the body of the
StateServer
take precedence over any functions written as a part of a state
module. In the case where there are competing function calls, your handler
functions written in the body of the StateServer
may emit :defer
as a
result, which will punt the processing of the event to the state modules.
# make sure query calls happen regardless of state
def handle_call(:query, _from, _state, data) do
{:reply, {state, data}}
end
# for all other call payloads, send to the state modules
def handle_call(_, _, _, _) do
:defer
end
defstate Start, for: :start do
def handle_call(...) do...
since this is a common pattern, we provide a defer
macro which is
equivalent to the above:
# make sure query calls happen regardless of state
def handle_call(:query, _from, _state, data) do
{:reply, {state, data}}
end
# for all other call payloads, send to the state modules
defer handle_call
Important
If you handle an event via any instance of a handler function block in
the main StateServer
module, and return a :reply
or :noreply
, it
will not be handled by the State
module, you must explicitly
specify :defer
to be handled by State
modules.
If there are no instances of the handler function, then handling will
default to the State
modules without using the defer
macro.
Defer with common events
If you would like to perform analysis on the inbound data, generating events
and defer to the individual states for further state-specific event
processing, you may do so with the {:defer, events}
result type. For
example, the following code:
# perform some common processing
def handle_call({:query, payload}, _from, _state, data) do
common_events = generate_common_events_from(payload)
{:defer, common_events}
end
defstate Start, for: :start do
def handle_call({:query, payload}, _from, data) do
# ...some code here...
{:reply, result, start_events}
end
end
Will result in the event stream common_events ++ start_events
emitted
when {:query, payload}
is called to the state server, with the following
exception:
- If you have an
{:update, <new_data>}
in the first position, or in the second position with a{:goto, <state>}
, or{:transition, <state>}
in the first position, the update event will be reflected in the deferred state machine call.
Termination rules
If a State module implements the terminate/2
callback, then it will be
called on termination. If it does not, termination will follow the parent
StateServer's StateServer.terminate/3
if it exists. Otherwise, no
action will be taken on terminate.
Example
The following code should produce a "light switch" state server that announces when it's been flipped.
defmodule SwitchWithStates do
@doc """
implements a light switch as a state server. In data, it keeps a count of
how many times the state of the light switch has changed.
On transition, it sends to standard error a comment that it has been flipped.
Note that the implementations are different between the two states.
"""
use StateServer, off: [flip: :on],
on: [flip: :off]
@type data :: non_neg_integer
def start_link, do: StateServer.start_link(__MODULE__, :ok)
@impl true
def init(:ok), do: {:ok, 0}
def flip(srv), do: StateServer.call(srv, :flip)
def query(srv), do: StateServer.call(srv, :query)
@impl true
def handle_call(:flip, _from, _state, _count) do
{:reply, :ok, transition: :flip}
end
defer handle_call
# we must defer the handle_call statement because there are both shared and
# individual implementation of handle_call features.
defstate Off, for: :off do
@impl true
def handle_transition(:flip, count) do
IO.puts(:stderr, "switch #{inspect self()} flipped on, #{count} times turned on")
{:noreply, update: count + 1}
end
@impl true
def handle_call(:query, _from, _count) do
{:reply, "state is off"}
end
end
defstate On, for: :on do
@impl true
def handle_transition(:flip, count) do
IO.puts(:stderr, "switch #{inspect self()} flipped off, #{count} times turned on")
:noreply
end
@impl true
def handle_call(:query, _from, _count) do
{:reply, "state is on"}
end
end
end
Link to this section Summary
Link to this section Callbacks
on_state_entry(transition, data)
View Source (optional)on_state_entry(transition :: atom(), data :: term()) :: StateServer.on_state_entry_response()