This section is to be read with the
Established Automata Theory does not deal much with how a state transition is triggered, but assumes that the output is a function of the input (and the state) and that they are some kind of values.
For an Event-Driven State Machine, the input is an event that triggers a state transition and the output is actions executed during the state transition. It can analogously to the mathematical model of a Finite-State Machine be described as a set of relations of the following form:
State(S) x Event(E) -> Actions(A), State(S')
These relations are interpreted as follows:
if we are in state
As
Like most
The callback module contains functions that implement
the state machine.
When an event occurs,
the
The behaviour engine holds the state machine state, server data, timer references, a queue of posponed messages and other metadata. It receives all process messages, handles the system messages, and calls the callback module with machine specific events.
The
Events are handled by one callback functions per state.
Events are handled by one single callback function.
The callback mode is selected at server start and may be changed with a code upgrade/downgrade.
See the section
The callback mode is selected by implementing a callback function
The
The short version: choose
The two
This can be done, for example, by focusing on one state at the time and for every state ensure that all events are handled. Alternatively, you can focus on one event at the time and ensure that it is handled in every state. You can also use a mix of these strategies.
With
This mode fits well when you have a regular state diagram, like the ones in this chapter, which describes all events and actions belonging to a state visually around that state, and each state has its unique name.
With
This mode works equally well when you want to focus on
one event at the time or on
one state at the time, but function
The mode enables the use of non-atom states, for example,
complex states or even hierarchical states.
If, for example, a state diagram is largely alike
for the client side and the server side of a protocol,
you can have a state
Which callback function that handles an event depends on the callback mode:
This form is the one mostly used in the
See section
The state is either the name of the function itself or an argument to it.
The other arguments are the
State enter calls are also handled by the event handler and have
slightly different arguments. See the section
The event handler return values are defined in the description of
Set next state and update the server data.
If the
See section
If
Same as the
Same as the
Same as the
Stop the server with reason
Same as the
To decide the first state the
In the first
There are more specific state-transition actions
that a callback function can command the
For details, see the
Events are categorized in different
The following is a complete list of event types and where they come from:
The
StateName(enter, OldState, Data) -> ... code for state enter actions here ... {keep_state, NewData}; StateName(EventType, EventContent, Data) -> ... code for actions here ... {next_state, NewStateName, NewData}.
Since the state enter call is not an event there are restrictions
on the allowed return value and
The first state that is entered will get a state enter call
with
You may repeat the state enter call using the
Depending on how your state machine is specified,
this can be a very useful feature,
but it forces you to handle the state enter calls in all states.
See also the
A door with a code lock can be seen as a state machine. Initially, the door is locked. When someone presses a button, an event is generated. The pressed buttons are collected, up to the number of buttons in the correct code. If correct, the door is unlocked for 10 seconds (10,000 milliseconds). If not correct, we wait for a new button to be pressed.
This code lock state machine can be implemented using
gen_statem:start_link({local,?NAME}, ?MODULE, Code, []).
button(Button) ->
gen_statem:cast(?NAME, {button,Button}).
init(Code) ->
do_lock(),
Data = #{code => Code, length => length(Code), buttons => []},
{ok, locked, Data}.
callback_mode() ->
state_functions.
]]>
NewButtons =
if
length(Buttons) < Length ->
Buttons;
true ->
tl(Buttons)
end ++ [Button],
if
NewButtons =:= Code -> % Correct
do_unlock(),
{next_state, open, Data#{buttons := []},
[{state_timeout,10000,lock}]};
true -> % Incomplete | Incorrect
{next_state, locked, Data#{buttons := NewButtons}}
end.
]]>
do_lock(),
{next_state, locked, Data};
open(cast, {button,_}, Data) ->
{next_state, open, Data}.
]]>
io:format("Lock~n", []).
do_unlock() ->
io:format("Unlock~n", []).
terminate(_Reason, State, _Data) ->
State =/= locked andalso do_lock(),
ok.
]]>
The code is explained in the next sections.
In the example in the previous section,
gen_statem:start_link({local,?NAME}, ?MODULE, Code, []).
]]>
The first argument,
If the name is omitted, the
The second argument,
The interface functions (
The third argument,
The fourth argument,
If name registration succeeds, the new
do_lock(),
Data = #{code => Code, length => length(Code), buttons => []},
{ok, locked, Data}.
]]>
Function
Function
Function
state_functions.
]]>
The function notifying the code lock about a button event is
implemented using
gen_statem:cast(?NAME, {button,Digit}).
]]>
The first argument is the name of the
The event is sent to the
NewButtons =
if
length(Buttons) < Length ->
Buttons;
true ->
tl(Buttons)
end ++ [Button],
if
NewButtons =:= Code -> % Correct
do_unlock(),
{next_state, open, Data#{buttons := []},
[{state_timeout,10000,lock}]};
true -> % Incomplete | Incorrect
{next_state, locked, Data#{buttons := NewButtons}}
end.
]]>
In state
When changing to state
{next_state, open, Data}.
]]>
In state
When a correct code has been given, the door is unlocked and
the following tuple is returned from
10,000 is a time-out value in milliseconds.
After this time (10 seconds), a time-out occurs.
Then,
do_lock(),
{next_state, locked, Data};
]]>
The timer for a state time-out is automatically cancelled
when the state machine changes states. You can restart
a state time-out by setting it to a new time, which cancels
the running timer and starts a new. This implies that
you can cancel a state time-out by restarting it with
time
Sometimes events can arrive in any state of the
Consider a
gen_statem:call(?NAME, code_length).
...
locked(...) -> ... ;
locked(EventType, EventContent, Data) ->
handle_common(EventType, EventContent, Data).
...
open(...) -> ... ;
open(EventType, EventContent, Data) ->
handle_common(EventType, EventContent, Data).
handle_common({call,From}, code_length, #{code := Code} = Data) ->
{keep_state, Data, [{reply,From,length(Code)}]}.
]]>
Another way to do it is through a convenience macro
gen_statem:call(?NAME, code_length).
-define(HANDLE_COMMON,
?FUNCTION_NAME(T, C, D) -> handle_common(T, C, D)).
%%
handle_common({call,From}, code_length, #{code := Code} = Data) ->
{keep_state, Data, [{reply,From,length(Code)}]}.
...
locked(...) -> ... ;
?HANDLE_COMMON.
...
open(...) -> ... ;
?HANDLE_COMMON.
]]>
This example uses
If the common event handler needs to know the current state
a function
handle_common(T, C, ?FUNCTION_NAME, D)).
]]>
If mode
handle_event_function.
handle_event(cast, {button,Digit}, State, #{code := Code} = Data) ->
case State of
locked ->
#{length := Length, buttons := Buttons} = Data,
NewButtons =
if
length(Buttons) < Length ->
Buttons;
true ->
tl(Buttons)
end ++ [Button],
if
NewButtons =:= Code -> % Correct
do_unlock(),
{next_state, open, Data#{buttons := []},
[{state_timeout,10000,lock}]};
true -> % Incomplete | Incorrect
{keep_state, Data#{buttons := NewButtons}}
end;
open ->
keep_state_and_data
end;
handle_event(state_timeout, lock, open, Data) ->
do_lock(),
{next_state, locked, Data}.
...
]]>
If the
If it is necessary to clean up before termination, the shutdown
strategy must be a time-out value and the
process_flag(trap_exit, true),
do_lock(),
...
]]>
When ordered to shut down, the
In this example, function
State =/= locked andalso do_lock(),
ok.
]]>
If the
gen_statem:stop(?NAME).
]]>
This makes the
A time-out feature inherited from
It is ordered by the state transition action
This type of time-out is useful for example to act on inactivity. Let us restart the code sequence if no button is pressed for say 30 seconds:
{next_state, locked, Data#{buttons := []}};
locked(
cast, {button,Digit},
#{code := Code, length := Length, buttons := Buttons} = Data) ->
...
true -> % Incomplete | Incorrect
{next_state, locked, Data#{buttons := NewButtons},
30000}
...
]]>
Whenever we receive a button event we start an event time-out
of 30 seconds, and if we get an event type
An event time-out is cancelled by any other event so you either get some other event or the time-out event. It is therefore not possible nor needed to cancel or restart an event time-out. Whatever event you act on has already cancelled the event time-out...
Note that an event time-out does not work well with
when you have for example a status call as in
The previous example of state time-outs only work if the state machine stays in the same state during the time-out time. And event time-outs only work if no disturbing unrelated events occur.
You may want to start a timer in one state and respond
to the time-out in another, maybe cancel the time-out
without changing states, or perhaps run multiple
time-outs in parallel. All this can be accomplished with
Here is how to accomplish the state time-out
in the previous example by instead using a generic time-out
named for example
...
if
NewButtons =:= Code -> % Correct
do_unlock(),
{next_state, open, Data#{buttons := []},
[{{timeout,open},10000,lock}]};
...
open({timeout,open}, lock, Data) ->
do_lock(),
{next_state,locked,Data};
open(cast, {button,_}, Data) ->
{keep_state,Data};
...
]]>
An specific generic time-out can just as a
In this particular case we do not need to cancel the timeout
since the timeout event is the only possible reason to
change the state from
Instead of bothering with when to cancel a time-out, a late time-out event can be handled by ignoring it if it arrives in a state where it is known to be late.
The most versatile way to handle time-outs is to use
Erlang Timers; see
Here is how to accomplish the state time-out in the previous example by instead using an Erlang Timer:
...
if
NewButtons =:= Code -> % Correct
do_unlock(),
Tref = erlang:start_timer(10000, self(), lock),
{next_state, open, Data#{buttons := [], timer => Tref}};
...
open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) ->
do_lock(),
{next_state,locked,maps:remove(timer, Data)};
open(cast, {button,_}, Data) ->
{keep_state,Data};
...
]]>
Removing the
If you need to cancel a timer because of some other event, you can use
Another way to handle a late time-out can be to not cancel it, but to ignore it if it arrives in a state where it is known to be late.
If you want to ignore a particular event in the current state
and handle it in a future state, you can postpone the event.
A postponed event is retried after the state has
changed, that is,
Postponing is ordered by the state transition
In this example, instead of ignoring button events
while in the
{keep_state,Data,[postpone]};
...
]]>
Since a postponed event is only retried after a state change,
you have to think about where to keep a state data item.
You can keep it in the server
This is not important if you do not postpone events. But if you later decide to start postponing some events, then the design flaw of not having separate states when they should be, might become a hard to find bug.
It is not uncommon that a state diagram does not specify how to handle events that are not illustrated in a particular state in the diagram. Hopefully this is described in an associated text or from the context.
Possible actions: ignore as in drop the event (maybe log it) or deal with the event in some other state as in postpone it.
Erlang's selective receive statement is often used to describe simple state machine examples in straightforward Erlang code. The following is a possible implementation of the first example:
spawn(
fun () ->
true = register(?NAME, self()),
do_lock(),
locked(Code, length(Code), [])
end).
button(Button) ->
?NAME ! {button,Button}.
]]>
receive
{button,Button} ->
NewButtons =
if
length(Buttons) < Length ->
Buttons;
true ->
tl(Buttons)
end ++ [Button],
if
NewButtons =:= Code -> % Correct
do_unlock(),
open(Code, Length);
true -> % Incomplete | Incorrect
locked(Code, Length, NewButtons)
end
end.
]]>
receive
after 10000 ->
do_lock(),
locked(Code, Length, [])
end.
do_lock() ->
io:format("Locked~n", []).
do_unlock() ->
io:format("Open~n", []).
]]>
The selective receive in this case causes implicitly
A selective receive cannot be used from a
The
Both mechanisms have the same theoretical time and memory complexity, while the selective receive language construct has smaller constant factors.
Say you have a state machine specification
that uses state enter actions.
Allthough you can code this using inserted events
(described in the next section), especially if just
one or a few states has got state enter actions,
this is a perfect use case for the built in
You return a list containing
process_flag(trap_exit, true),
Data = #{code => Code, length = length(Code)},
{ok, locked, Data}.
callback_mode() ->
[state_functions,state_enter].
locked(enter, _OldState, Data) ->
do_lock(),
{keep_state,Data#{buttons => []}};
locked(
cast, {button,Digit},
#{code := Code, length := Length, buttons := Buttons} = Data) ->
...
if
NewButtons =:= Code -> % Correct
{next_state, open, Data};
...
open(enter, _OldState, _Data) ->
do_unlock(),
{keep_state_and_data, [{state_timeout,10000,lock}]};
open(state_timeout, lock, Data) ->
{next_state, locked, Data};
...
]]>
You can repeat the state enter code by returning one of
It can sometimes be beneficial to be able to generate events
to your own state machine.
This can be done with the
You can generate events of any existing
One example for this is to pre-process incoming data, for example decrypting chunks or collecting characters up to a line break.
Purists may argue that this should be modelled with a separate state machine that sends pre-processed events to the main state machine, but to decrease overhead the small pre-processing state machine can be implemented in the common state event handling of the main state machine using a few state data variables that then sends the pre-processed events as internal events to the main state machine. Using internal events also can make it easier to synchronize the state machines.
The following example uses an input model where the buttons generate up/down events and the lock responds to an up event after the corresponding down event.
gen_statem:cast(?NAME, {down,Button}).
up(button) ->
gen_statem:cast(?NAME, {up,Button}).
...
locked(enter, _OldState, Data) ->
do_lock(),
{keep_state,Data#{remaining => Code, buf => []}};
locked(
internal, {button,Digit},
#{code := Code, length := Length, buttons := Buttons} = Data) ->
...
]]>
{keep_state, Data#{button := Button}};
handle_common(cast, {up,Button}, Data) ->
case Data of
#{button := Button} ->
{keep_state,maps:remove(button, Data),
[{next_event,internal,{button,Button}}]};
#{} ->
keep_state_and_data
end;
...
open(internal, {button,_}, Data) ->
{keep_state,Data,[postpone]};
...
]]>
If you start this program with
This section includes the example after most of the mentioned modifications and some more using state enter calls, which deserves a new state diagram:
Notice that this state diagram does not specify how to handle
a button event in the state
Using state functions:
gen_statem:start_link({local,?NAME}, ?MODULE, Code, []).
stop() ->
gen_statem:stop(?NAME).
down(Digit) ->
gen_statem:cast(?NAME, {down,Digit}).
up(Digit) ->
gen_statem:cast(?NAME, {up,Digit}).
code_length() ->
gen_statem:call(?NAME, code_length).
]]>
process_flag(trap_exit, true),
Data = #{code => Code, length => length(Code), buttons => []},
{ok, locked, Data}.
callback_mode() ->
[state_functions,state_enter].
-define(HANDLE_COMMON,
?FUNCTION_NAME(T, C, D) -> handle_common(T, C, D)).
%%
handle_common(cast, {down,Button}, Data) ->
{keep_state, Data#{button => Button}};
handle_common(cast, {up,Button}, Data) ->
case Data of
#{button := Button} ->
{keep_state, maps:remove(button, Data),
[{next_event,internal,{button,Data}}]};
#{} ->
keep_state_and_data
end;
handle_common({call,From}, code_length, #{code := Code}) ->
{keep_state_and_data, [{reply,From,length(Code)}]}.
]]>
do_lock(),
{keep_state, Data#{buttons := []}};
locked(state_timeout, button, Data) ->
{keep_state, Data#{buttons := []}};
locked(
internal, {button,Digit},
#{code := Code, length := Length, buttons := Buttons} = Data) ->
NewButtons =
if
length(Buttons) < Length ->
Buttons;
true ->
tl(Buttons)
end ++ [Button],
if
NewButtons =:= Code -> % Correct
do_unlock(),
{next_state, open, Data};
true -> % Incomplete | Incorrect
{keep_state, Data#{buttons := NewButtons},
[{state_timeout,30000,button}]}
end;
?HANDLE_COMMON.
]]>
do_unlock(),
{keep_state_and_data, [{state_timeout,10000,lock}]};
open(state_timeout, lock, Data) ->
{next_state, locked, Data};
open(internal, {button,_}, _) ->
{keep_state_and_data, [postpone]};
?HANDLE_COMMON.
do_lock() ->
io:format("Locked~n", []).
do_unlock() ->
io:format("Open~n", []).
terminate(_Reason, State, _Data) ->
State =/= locked andalso do_lock(),
ok.
]]>
This section describes what to change in the example
to use one
[handle_event_function,state_enter].
]]>
do_lock(),
{keep_state, Data#{buttons := []}};
handle_event(state_timeout, button, locked, Data) ->
{keep_state, Data#{buttons := []}};
handle_event(
internal, {button,Digit}, locked,
#{code := Code, length := Length, buttons := Buttons} = Data) ->
NewButtons =
if
length(Buttons) < Length ->
Buttons;
true ->
tl(Buttons)
end ++ [Button],
if
NewButtons =:= Code -> % Correct
do_unlock(),
{next_state, open, Data};
true -> % Incomplete | Incorrect
{keep_state, Data#{buttons := NewButtons},
[{state_timeout,30000,button}]}
end;
]]>
do_unlock(),
{keep_state_and_data, [{state_timeout,10000,lock}]};
handle_event(state_timeout, lock, open, Data) ->
{next_state, locked, Data};
handle_event(internal, {button,_}, open, _) ->
{keep_state_and_data,[postpone]};
]]>
{keep_state, Data#{button => Button}};
handle_event(cast, {up,Button}, _State, Data) ->
case Data of
#{button := Button} ->
{keep_state, maps:remove(button, Data),
[{state_timeout,30000,button}]};
#{} ->
keep_state_and_data
end;
handle_event({call,From}, code_length, _State, #{length := Length}) ->
{keep_state_and_data, [{reply,From,Length}]}.
]]>
Notice that postponing buttons from the
The example servers so far in this chapter print the full internal state in the error log, for example, when killed by an exit signal or because of an internal error. This state contains both the code lock code and which digits that remain to unlock.
This state data can be regarded as sensitive, and maybe not what you want in the error log because of some unpredictable event.
Another reason to filter the state can be that the state is too large to print, as it fills the error log with uninteresting details.
To avoid this, you can format the internal state
that gets in the error log and gets returned from
StateData =
{State,
maps:filter(
fun (code, _) -> false;
(_, _) -> true
end,
Data)},
case Opt of
terminate ->
StateData;
normal ->
[{data,[{"State",StateData}]}]
end.
]]>
It is not mandatory to implement a
The callback mode
One reason to use this is when you have a state item
that when changed should cancel the
Suppose now that we call
code_lock:start_link([a,b,c], x).
{ok,<0.666.0>}
2> code_lock:button(a).
ok
3> code_lock:button(b).
ok
4> code_lock:button(c).
ok
Open
5> code_lock:button(y).
ok
6> code_lock:set_lock_button(y).
x
% What should happen here? Immediate lock or nothing?
]]>
We could say that the button was pressed too early so it is not to be recognized as the lock button. Or we can make the lock button part of the state so when we then change the lock button in the locked state, the change becomes a state change and all postponed events are retried, therefore the lock is immediately locked!
We define the state as
gen_statem:start_link(
{local,?NAME}, ?MODULE, {Code,LockButton}, []).
stop() ->
gen_statem:stop(?NAME).
button(Button) ->
gen_statem:cast(?NAME, {button,Button}).
set_lock_button(LockButton) ->
gen_statem:call(?NAME, {set_lock_button,LockButton}).
]]>
process_flag(trap_exit, true),
Data = #{code => Code, length => length(Code), buttons => []},
{ok, {locked,LockButton}, Data}.
callback_mode() ->
[handle_event_function,state_enter].
%% State: locked
handle_event(enter, _OldState, {locked,_}, Data) ->
do_lock(),
{keep_state, Data#{buttons := []}};
handle_event(state_timeout, button, {locked,_}, Data) ->
{keep_state, Data#{buttons := []}};
handle_event(
cast, {button,Digit}, {locked,LockButton},
#{code := Code, length := Length, buttons := Buttons} = Data) ->
NewButtons =
if
length(Buttons) < Length ->
Buttons;
true ->
tl(Buttons)
end ++ [Button],
if
NewButtons =:= Code -> % Correct
do_unlock(),
{next_state, {open,LockButton}, Data};
true -> % Incomplete | Incorrect
{keep_state, Data#{buttons := NewButtons},
[{state_timeout,30000,button}]}
end;
]]>
do_unlock(),
{keep_state_and_data,
[{state_timeout,10000,lock}]};
handle_event(state_timeout, lock, {open,_}, Data) ->
{next_state, locked, Data};
handle_event(cast, {button,LockButton}, {open,LockButton}, Data) ->
{next_state, {locked,LockButton}, Data};
handle_event(cast, {button,_}, {open,_}, Data) ->
{keep_state_and_data,[postpone]};
]]>
{next_state, {StateName,NewLockButton}, Data,
[{reply,From,OldLockButton}]}.
]]>
io:format("Locked~n", []).
do_unlock() ->
io:format("Open~n", []).
terminate(_Reason, State, _Data) ->
State =/= locked andalso do_lock(),
ok.
]]>
If you have many servers in one node
and they have some state(s) in their lifetime in which
the servers can be expected to idle for a while,
and the amount of heap memory all these servers need
is a problem, then the memory footprint of a server
can be mimimized by hibernating it through
It is rather costly to hibernate a process; see
We can in this example hibernate in the
do_unlock(),
{keep_state_and_data,
[{state_timeout,10000,lock},
hibernate]};
...
]]>
The atom
To change that we would need to insert
action
Another not uncommon scenario is to use the event time-out
to trigger hibernation after a certain time of inactivity.
There is also a server start option
This particular server probably does not use heap memory worth hibernating for. To gain anything from hibernation, your server would have to produce non-insignificant garbage during callback execution, for which this example server can serve as a bad example.