This section is to be read with the
This is a new behavior in Erlang/OTP 19.0. It has been thoroughly reviewed, is stable enough to be used by at least two heavy OTP applications, and is here to stay. Depending on user feedback, we do not expect but can find it necessary to make minor not backward compatible changes into Erlang/OTP 20.0.
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
In mode
StateName(EventType, EventContent, Data) -> .. code for actions here ... {next_state, NewStateName, NewData}.
In mode
handle_event(EventType, EventContent, State, Data) -> .. code for actions here ... {next_state, State', Data'}
Both these modes allow other return tuples; see
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
This example starts off as equivalent to the example in section
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. Depending on what buttons have been pressed before, the sequence so far can be correct, incomplete, or wrong. If correct, the door is unlocked for 10 seconds (10,000 milliseconds). If incomplete, we wait for another button to be pressed. If wrong, we start all over, waiting for a new button sequence.
This code lock state machine can be implemented using
gen_statem:start_link({local,?NAME}, ?MODULE, Code, []).
button(Digit) ->
gen_statem:cast(?NAME, {button,Digit}).
init(Code) ->
do_lock(),
Data = #{code => Code, remaining => Code},
{?CALLBACK_MODE,locked,Data}.
locked(
cast, {button,Digit},
#{code := Code, remaining := Remaining} = Data) ->
case Remaining of
[Digit] ->
do_unlock(),
{next_state,open,Data#{remaining := Code},10000};
[Digit|Rest] -> % Incomplete
{next_state,locked,Data#{remaining := Rest}};
_Wrong ->
{next_state,locked,Data#{remaining := Code}}
end.
open(timeout, _, Data) ->
do_lock(),
{next_state,locked,Data};
open(cast, {button,_}, Data) ->
do_lock(),
{next_state,locked,Data}.
do_lock() ->
io:format("Lock~n", []).
do_unlock() ->
io:format("Unlock~n", []).
terminate(_Reason, State, _Data) ->
State =/= locked andalso do_lock(),
ok.
code_change(_Vsn, State, Data, _Extra) ->
{?CALLBACK_MODE,State,Data}.
]]>
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, remaining => Code},
{?CALLBACK_MODE,locked,Data}.
]]>
Function
Function
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 made into a message and sent to the
case Remaining of
[Digit] -> % Complete
do_unlock(),
{next_state,open,Data#{remaining := Code},10000};
[Digit|Rest] -> % Incomplete
{next_state,locked,Data#{remaining := Rest}};
[_|_] -> % Wrong
{next_state,locked,Data#{remaining := Code}}
end.
open(timeout, _, Data) ->
do_lock(),
{next_state,locked,Data};
open(cast, {button,_}, Data) ->
do_lock(),
{next_state,locked,Data}.
]]>
If the door is locked and a button is pressed, the pressed
button is compared with the next correct button.
Depending on the result, the door is either unlocked
and the
If the pressed button is incorrect, the server data restarts from the start of the code sequence.
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};
]]>
Sometimes events can arrive in any state of the
Consider a
gen_statem:call(?NAME, code_length).
...
locked(...) -> ... ;
locked(EventType, EventContent, Data) ->
handle_event(EventType, EventContent, Data).
...
open(...) -> ... ;
open(EventType, EventContent, Data) ->
handle_event(EventType, EventContent, Data).
handle_event({call,From}, code_length, #{code := Code} = Data) ->
{keep_state,Data,[{reply,From,length(Code)}]}.
]]>
This example uses
If mode
case State of
locked ->
case maps:get(remaining, Data) of
[Digit] -> % Complete
do_unlock(),
{next_state,open,Data#{remaining := Code},10000};
[Digit|Rest] -> % Incomplete
{keep_state,Data#{remaining := Rest}};
[_|_] -> % Wrong
{keep_state,Data#{remaining := Code}}
end;
open ->
do_lock(),
{next_state,locked,Data}
end;
handle_event(timeout, _, 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(),
...
]]>
In the following example, function
State =/= locked andalso do_lock(),
ok.
]]>
If the
gen_statem:stop(?NAME).
]]>
This makes the
In the first sections actions were mentioned as a part of
the general state machine model. These actions
are implemented with the code that callback module
There are more specific state-transition actions
that a callback function can order the
In the example earlier was mentioned the event time-out
and replying to a caller.
An example of event postponing is included later in this chapter.
For details, see the
The previous sections mentioned a few
The following is a complete list of event types and where they come from:
The time-out event generated by state transition action
Often you want a timer not to be cancelled by any event
or you want to start a timer in one state and respond
to the time-out in another. This can be accomplished
with a regular Erlang timer:
For the example so far in this chapter: using the
Suppose that we do not want a button to lock the door,
instead we want to ignore button events in the
case Remaining of
[Digit] ->
do_unlock(),
Tref = erlang:start_timer(10000, self(), lock),
{next_state,open,Data#{remaining := Code, timer := Tref}};
...
open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) ->
do_lock(),
{next_state,locked,Data};
open(cast, {button,_}, Data) ->
{keep_state,Data};
...
]]>
If you need to cancel a timer because of some other event, you can use
Another way to cancel a timer is not to 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]};
...
]]>
A postponed event is only retried after a state change
translates into a requirement on the event and state space.
If you have a choice between storing a state data item
in the
You want to avoid that you maybe much later decide
to postpone an event in one state and by misfortune it is never retried,
as the code only changes the
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, Code)
end).
button(Digit) ->
?NAME ! {button,Digit}.
locked(Code, [Digit|Remaining]) ->
receive
{button,Digit} when Remaining =:= [] ->
do_unlock(),
open(Code);
{button,Digit} ->
locked(Code, Remaining);
{button,_} ->
locked(Code, Code)
end.
open(Code) ->
receive
after 10000 ->
do_lock(),
locked(Code, Code)
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 state transition
Both mechanisms have the same theoretical time and memory complexity, while the selective receive language construct has smaller constant factors.
It can sometimes be beneficial to be able to generate events
to your own state machine.
This can be done with the state transition
You can generate events of any existing
One example of using self-generated events can be when you have
a state machine specification that uses state entry actions.
You can code that using a dedicated function
to do the state transition. But if you want that code to be
visible besides the other state logic, you can insert
an
The following is an implementation of entry actions
using
process_flag(trap_exit, true),
Data = #{code => Code},
enter(?CALLBACK_MODE, locked, Data).
...
locked(internal, enter, _Data) ->
do_lock(),
{keep_state,Data#{remaining => Code}};
locked(
cast, {button,Digit},
#{code := Code, remaining := Remaining} = Data) ->
case Remaining of
[Digit] ->
enter(next_state, open, Data);
...
open(internal, enter, _Data) ->
Tref = erlang:start_timer(10000, self(), lock),
do_unlock(),
{keep_state,Data#{timer => Tref}};
open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) ->
enter(next_state, locked, Data);
...
enter(Tag, State, Data) ->
{Tag,State,Data,[{next_event,internal,enter}]}.
]]>
This section includes the example after all mentioned modifications and some more using the entry actions, 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).
button(Digit) ->
gen_statem:cast(?NAME, {button,Digit}).
code_length() ->
gen_statem:call(?NAME, code_length).
init(Code) ->
process_flag(trap_exit, true),
Data = #{code => Code},
enter(?CALLBACK_MODE, locked, Data).
locked(internal, enter, #{code := Code} = Data) ->
do_lock(),
{keep_state,Data#{remaining => Code}};
locked(
cast, {button,Digit},
#{code := Code, remaining := Remaining} = Data) ->
case Remaining of
[Digit] -> % Complete
enter(next_state, open, Data);
[Digit|Rest] -> % Incomplete
{keep_state,Data#{remaining := Rest}};
[_|_] -> % Wrong
{keep_state,Data#{remaining := Code}}
end;
locked(EventType, EventContent, Data) ->
handle_event(EventType, EventContent, Data).
open(internal, enter, Data) ->
Tref = erlang:start_timer(10000, self(), lock),
do_unlock(),
{keep_state,Data#{timer => Tref}};
open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) ->
enter(next_state, locked, Data);
open(cast, {button,_}, _) ->
{keep_state_and_data,[postpone]};
open(EventType, EventContent, Data) ->
handle_event(EventType, EventContent, Data).
handle_event({call,From}, code_length, #{code := Code}) ->
{keep_state_and_data,[{reply,From,length(Code)}]}.
enter(Tag, State, Data) ->
{Tag,State,Data,[{next_event,internal,enter}]}.
do_lock() ->
io:format("Locked~n", []).
do_unlock() ->
io:format("Open~n", []).
terminate(_Reason, State, _Data) ->
State =/= locked andalso do_lock(),
ok.
code_change(_Vsn, State, Data, _Extra) ->
{?CALLBACK_MODE,State,Data}.
]]>
This section describes what to change in the example
to use one
do_lock(),
{keep_state,Data#{remaining => Code}};
handle_event(
cast, {button,Digit}, locked,
#{code := Code, remaining := Remaining} = Data) ->
case Remaining of
[Digit] -> % Complete
enter(next_state, open, Data);
[Digit|Rest] -> % Incomplete
{keep_state,Data#{remaining := Rest}};
[_|_] -> % Wrong
{keep_state,Data#{remaining := Code}}
end;
%%
%% State: open
handle_event(internal, enter, open, Data) ->
Tref = erlang:start_timer(10000, self(), lock),
do_unlock(),
{keep_state,Data#{timer => Tref}};
handle_event(info, {timeout,Tref,lock}, open, #{timer := Tref} = Data) ->
enter(next_state, locked, Data);
handle_event(cast, {button,_}, open, _) ->
{keep_state_and_data,[postpone]};
%%
%% Any state
handle_event({call,From}, code_length, _State, #{code := Code}) ->
{keep_state_and_data,[{reply,From,length(Code)}]}.
...
]]>
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;
(remaining, _) -> 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 affects the event handling,
in particular in combination with postponing events.
We complicate the previous example
by introducing a configurable lock button
(this is the state item in question),
which in the
Suppose now that we call
So we make the
If a process now calls
We define the state as
gen_statem:start_link(
{local,?NAME}, ?MODULE, {Code,LockButton}, []).
stop() ->
gen_statem:stop(?NAME).
button(Digit) ->
gen_statem:call(?NAME, {button,Digit}).
code_length() ->
gen_statem:call(?NAME, code_length).
set_lock_button(LockButton) ->
gen_statem:call(?NAME, {set_lock_button,LockButton}).
init({Code,LockButton}) ->
process_flag(trap_exit, true),
Data = #{code => Code, remaining => undefined, timer => undefined},
enter(?CALLBACK_MODE, {locked,LockButton}, Data, []).
handle_event(
{call,From}, {set_lock_button,NewLockButton},
{StateName,OldLockButton}, Data) ->
{next_state,{StateName,NewLockButton},Data,
[{reply,From,OldLockButton}]};
handle_event(
{call,From}, code_length,
{_StateName,_LockButton}, #{code := Code}) ->
{keep_state_and_data,
[{reply,From,length(Code)}]};
handle_event(
EventType, EventContent,
{locked,LockButton}, #{code := Code, remaining := Remaining} = Data) ->
case {EventType,EventContent} of
{internal,enter} ->
do_lock(),
{keep_state,Data#{remaining := Code}};
{{call,From},{button,Digit}} ->
case Remaining of
[Digit] -> % Complete
next_state(
{open,LockButton}, Data,
[{reply,From,ok}]);
[Digit|Rest] -> % Incomplete
{keep_state,Data#{remaining := Rest},
[{reply,From,ok}]};
[_|_] -> % Wrong
{keep_state,Data#{remaining := Code},
[{reply,From,ok}]}
end
end;
handle_event(
EventType, EventContent,
{open,LockButton}, #{timer := Timer} = Data) ->
case {EventType,EventContent} of
{internal,enter} ->
Tref = erlang:start_timer(10000, self(), lock),
do_unlock(),
{keep_state,Data#{timer := Tref}};
{info,{timeout,Timer,lock}} ->
next_state({locked,LockButton}, Data, []);
{{call,From},{button,Digit}} ->
if
Digit =:= LockButton ->
erlang:cancel_timer(Timer),
next_state(
{locked,LockButton}, Data,
[{reply,From,locked}]);
true ->
{keep_state_and_data,
[postpone]}
end
end.
next_state(State, Data, Actions) ->
enter(next_state, State, Data, Actions).
enter(Tag, State, Data, Actions) ->
{Tag,State,Data,[{next_event,internal,enter}|Actions]}.
do_lock() ->
io:format("Locked~n", []).
do_unlock() ->
io:format("Open~n", []).
terminate(_Reason, State, _Data) ->
State =/= locked andalso do_lock(),
ok.
code_change(_Vsn, State, Data, _Extra) ->
{?CALLBACK_MODE,State,Data}.
format_status(Opt, [_PDict,State,Data]) ->
StateData =
{State,
maps:filter(
fun (code, _) -> false;
(remaining, _) -> false;
(_, _) -> true
end,
Data)},
case Opt of
terminate ->
StateData;
normal ->
[{data,[{"State",StateData}]}]
end.
]]>
It can be an ill-fitting model for a physical code lock
that the
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
case {EventType,EventContent} of
{internal,enter} ->
Tref = erlang:start_timer(10000, self(), lock),
do_unlock(),
{keep_state,Data#{timer := Tref},[hibernate]};
...
]]>
The
To change that we would need to insert
action
This server probably does not use heap memory worth hibernating for. To gain anything from hibernation, your server would have to produce some garbage during callback execution, for which this example server can serve as a bad example.