Gen StateM
The
gen_statembehaviour is based on the Mealy State Machine. In versions before OTP 20, another behaviour called Gen FSM implemented a Moore State Machine. The Mealy machine implementation improved performance for all OTP libraries.This behavior is built on the server behavior (
gen_server) (…), also adding state management at the behavior level. The difference between the server and the state machine is that an event received by a server is handled in only one dimension, the implemented code and the only existing state data. In the state machine, the code that handles each request, and each event, depends on the internal state of the state machine.
Building Blocks
Callback Mode
The way to use
gen_statemdiffers depending on the way to handle the events that we decide to use. We can choose between state functions or the event handler function.The choice is made through the definition of the callback
callback_mode/0.
The callback_mode choice is essentially a choice between specialization and centralization.
| Feature | state_functions |
handle_event_function |
|---|---|---|
| Logic Structure | One function per state. | One single function for everything. |
| Argument Flow | The state name is the function name. | The state name is passed as an argument. |
State Functions
This basically a way to write a dedicated specialist function for every
state. When you are in the red state, only the red/3 (or red/4) function is
called.
You define a function that matches the name of your current state.
red(event) -> {next_state, green, NewData}.
Event Handlers
This is the "generalist" approach. One function (handle_event/4) answers the
phone regardless of what is happening.
- Every event, regardless of state, goes to the same block of code.
- Good for state machines where most events are handled the same way regardless of the state, or for very small machines where separate functions would feel like overkill.
State Enter
The Event/Action Life Cycle
The life cycle of a state machine is defined by its transitions, and each transition happens when an event is received. Lets see in detail what happens in the state machine when receiving a step event. Regardless of the event, the steps are as follows:
- We receive an event. The event is decomposed to indicate in the callback the type of request and the data. We add the state data and state name and execute the function.
- We receive the return from the function. The callback executed and gave us what to do next:
- We keep (
keep_state). There is no change of state.- We change or repeat state (
next_stateorrepeat_state). We change the state, it implies the following tasks:
- All state timers are stopped.
- If
state_enterwas configured, the execution of anenterevent with the event data of the previous state is added as an action.- Check if there are postponed events to repeat them.
- We stop the state machine (
stop).- We execute the actions defined in the return of the callback:
- Postpone. The current event enters the queue of postponed events to be relaunched when we change the state.
- Timers. Configure the timers indicated in the actions.
- Add the events (
next_event) to be fired immediately.Throughout the flow, we see how new events can occur from the processing of the state change or actions of the
next_eventtype. These new events will be executed before going back to the cycle of listening for new incoming events.
Event Types
As events we receive not only information from timers or messages but also structured information such as
call(synchronous calls) andcast(asynchronous calls) as we saw in Gen Server.
External
-type external_event_type() :: {call, From :: from()} | cast | info.
| Module | Callback |
|---|---|
gen_statem:cast(Server, Msg) |
Mod:StateName(cast, Msg, Data) |
gen_statem:call(Server, Req) |
Mod:StateName({call, From}, Req, Data) |
Server ! Msg |
Mod:StateName(info, Msg, Data) |
cast: Asynchronous call sent to the state machine, sent fromgen_statem:cast/2.{call, from()}: Synchronous call, comes either fromgen_statem:call/2orgen_statem:send_request/2.info: Sent viaerlang:send/2or with the!syntax, no specific format.
Timeout
-type timeout_event_type() :: timeout | {timeout, Name :: term()} | state_timeout.
| Message | Callback |
|---|---|
{timeout, Time, Msg} |
Mod:StateName(timeout, Msg, Data) |
{{timeout, Name}, Msg, Data} |
Mod:StateName({timeout, Name}, Msg, Data) |
{state_timeout, Msg, Data} |
Mod:StateName(state_timeout, Msg, Data) |
Internal
internal events can only be generated by the state machine itself through the transition action
next_event.
| Message | Callback |
|---|---|
{next_event, internal, Msg} |
Mod:StateName(internal, Msg, Data) |
Return Tuples / "Commands"
In gen_statem, your callback functions communicate their intent back to the
behavior engine using specific return tuples. Think of these as the "commands"
you give the state machine after handling an event.
Changing State
| Tuple | Description |
|---|---|
{next_state, State, Data, [Actions]} |
Transitions to a new state. Use when the machine's "mode" changes. |
If you aren't changing states, use keep_state rather than next_state. It’s more
idiomatic and better for documentation.
Staying Put / Continuations
Use these when you want to handle an event without leaving the current state.
| Tuple | Description |
|---|---|
{keep_state, Data, [Actions]} |
Keeps the current state name, but update the internal Data. |
keep_state_and_data |
Everything stays exactly as it was. Can also be used as {keep_state_and_data, Actions} |
Re-entering / Reset
These are special because they trigger state entry events. If you have logic that needs to run every time you "arrive" at a state, these returns will trigger it again.
| Tuple | Description |
|---|---|
{repeat_state, Data, [Actions]} |
Stays in the same state but executes the "enter" logic. |
repeat_state_and_data |
Re-enters the state without changing the data. |
Terminating / Stopping
| Tuple | Description |
|---|---|
stop / {stop, Reason, Data} |
Ends the process and moves to the terminate/3 callback. |
{stop_and_reply, Reason, Reply, Data} |
Similar to stop, but it sends a synchronous response back to a waiting caller before the process terminates. |
Actions
We can organize the actions in groups. These groups are not exclusive. We can add each action to none or several groups. We will also see actions not belonging to any group. The groups are:
- Input actions
- These actions include hibernation, timeout, and response actions.
- Timing actions
- These actions include event, generic and state timers.
- Transition actions
- These actions include postponing an event, hibernation, event timer, generic timer and state timer.
Actions are instructions returned alongside a state transition. They are grouped
in a list, and while most "conflicting" actions follow a "last one wins" rule,
others (like next_event) can be stacked.
postpone | {postpone, boolean()}- Buffers the current event in a separate queue. It is automatically retried only when the machine transitions to a new state. Essential for handling events that arrive "out of order."
{next_event, event_type(), event_content()}- Injects a new event into the
engine's internal queue. Unlike most actions, these are additive (you can
trigger multiple). Use
internalas the type to distinguish self-generated events from externalcallorcastsignals. hibernate | {hibernate, boolean()}- Forces the process into a sleep. The process wakes up instantly upon receiving the next event.
timeout() | {timeout, timeout(), event_content(), options()}- A nameless
"event timer", if it expires, it delivers a
timeoutevent. Starting a new nameless timer cancels the previous one. {{timeout, name()}, timeout(), event_content(), options()}- A "named timer",
allows multiple concurrent timers to run. Setting a timer with the same
Namereplaces the existing one. {state_timeout, timeout(), event_content(), options()}- A timer strictly
tied to the current state. If the state changes, this timer is automatically
cancelled unless the transition uses
keep_state. {reply, from(), term()}- Sends a synchronous response back to a client
waiting on a
gen_statem:call/2. Usually paired withFromdata extracted from the event content. {change_callback_module, module()}- Hot-swaps the logic. All future events
will be handled by
NewMod. {push_callback_module, module()}- Pushes the current module onto a stack and
switches to
NewMod. Perfect for creating "sub-modes" or hierarchical state machines. pop_callback_module- The "return" key. Drops the current module and reverts to the one previously on the stack.