For those truly, immensely important emails … use Obligd.

apr 28

Bend it like gen_statem

Although OTP hackers lean on ‘gen_server’ an order of magnitude more than other behaviors, it’s not uncommon to need the finite state machine pattern from time to time. At Obligd, the need for a state-machine comes up once in a blue moon. And the last time we needed to code one up, the now deprecated behavior, ‘gen_fsm,’ was the way to go about it.

The new kid on the block is ‘gen_statem,’ although arguably, it’s been a few releases already since first being bundled into OTP.
Seemingly, it’s quite fully featured, but can be quite daunting for first-timers. (Especially if one isn’t wholly confident with everything involved around ‘gen_server’ as ‘gen_statem’ feels like two complex behaviors mashed into one.)

You see, the challenge is being able to breathe ‘gen_server’ as if one has gills, so that one can keep the maze of their state-machine straight in their head. It helps to first mock up your state-machine in a truth-table, give it some thought, sleep on it, re-work it, before jumping into the code. If you do that, then the task of crafting a state machine using ‘gen_statem’ isn’t too grueling.

An example

We thought it would be fun to create a small introduction to ‘gen_statem’ for anyone who enjoys toy implementations as ice breakers. The goal is to utilize timers & transition only between a couple of states, so the source doesn’t boil over and lose anyone along the way.

Let’s code up a session server that meets the following specifications:

  1. Create a state-machine that could be used by other components to juggle a session for a given user (in this case, using an email to identify said user);
  2. After a new session has been requested, returned is a quasi-unique key. This key could be used in all sorts of ways, like embedded into an activation link that goes out in an email for a password-less login system (such details go beyond the scope of this particular tutorial);
  3. Upon key creation, the session server will timeout after ten minutes, simply exiting normal when it does;
  4. If a request comes in with the apt key that was sent out, then the session is ‘alive,’ and the requester is ‘logged in’;
  5. Similarly, this active session times out after some time, here, seventy minutes;
  6. If a ‘log-out’ request comes in, we should do some cleanup, and stop the apt session server instance;
  7. Desired as well, is a small routine to keep bumping the alive-state session from expiration (to be used by other components where needed).

With this back-of-the-envelope ‘spec,’ we are in good shape to carve out a truth-table for the logic within our state-machine. This little snippet serves us well, although it leaves a lot to be fleshed out :

Ref | State    | Action             | Re-action   |  State1
===========================================================
a   | init_sm  | —                  | give_key    | waiting
b   | waiting  | request_entry(Key) | set_session | alive
b1  | waiting  | expire()           | cleanup     | stop_sm
c   | alive    | request_depart()   | cleanup     | stop_sm
c1  | alive    | expire()           | cleanup     | stop_sm

We are using plain English to describe the transitions, although I suppose parts could port over to code pretty effortlessly. The columns are as follows: ‘State’ is from which state the transition comes from; ‘Action’ is an incoming request from outside the state-machine within that state (or in the case of timeouts, an action from a background timer created explicitly); ‘Re-action’ is the state-machine’s response in lieu of such actions; and ‘State1’ is the new state established if everything went according to plan.

You’ll notice that both ‘b1’ & ‘c1’ handle the timeout scenarios.

Coding it up

Here is a version implemented using the ‘gen_statem’ behavior :

-module(session_sm).
-behavior(gen_statem).

%% api
-export([start_link/1]).
-export([stop/0]).
-export([bump/0, request/1, request/2]).

%% callback
-export([init/1, callback_mode/0, terminate/3, code_change/4]).

%% state-callback
-export([waiting/3, alive/3]).

-define(REGISTERED, session_sm).
-define(CHARS, "ABCDEFGHIJKLMNPQRSTUVWXYZ"
               "abcdefghijklmnopqrstuvwxyz123456789").

-record(session, {email, key}).

%%
%% api routines
%%

start_link(<<Email/bitstring>>) ->
    gen_statem:start_link({local, ?REGISTERED}, ?MODULE, Email, []).

stop() ->
    gen_statem:stop(?REGISTERED).

bump() ->
    gen_statem:cast(?REGISTERED, bump).

request(key) ->
    gen_statem:call(?REGISTERED, key);
request(departure) ->
    gen_statem:cast(?REGISTERED, departure).

request(entry, Key) ->
    gen_statem:call(?REGISTERED, {entry, Key}).

%%
%% callback routines
%%

init(<<Email/bitstring>>) ->
    ok       = handle(start_statem),
    {ok, Ld} = handle(new_session, Email),
    Timeout  = session_duration(waiting),
    {ok, waiting, Ld, [{state_timeout, Timeout, hard_stop}]}.

callback_mode() ->
    state_functions.

code_change(_Vsn, State, Ld, _Extra) ->
    {ok, State, Ld}.

terminate(_Reason, State, #session{}=Ld) ->
    ok = handle_stop_session(State, Ld),
    handle(stop_statem).

%%
%% state-callback routines
%%

alive(cast, bump, #session{}=Ld) ->
    ok = handle(bump_session, Ld),
    Timeout = session_duration(alive),
    {keep_state, Ld, [{state_timeout, Timeout, hard_stop}]};
alive(cast, departure, #session{}=Ld) ->
    {stop, normal, Ld};
alive(state_timeout, hard_stop, #session{}=Ld) ->
    {stop, alive_timed_out, Ld};
alive(EventType, EventContent, #session{}=Ld) ->
    handle_event({EventType, EventContent, Ld}).

waiting({call, From}, key, #session{key=Key}=Ld) -> 
    Reply = {reply, From, {key, Key}},
    {keep_state, Ld, [Reply]};
waiting({call, From}, {entry, Key}, #session{}=Ld) ->
    {ok, Ld1} = handle(start_session, {Key, Ld}),
    Reply     = {reply, From, ok},
    Timeout   = session_duration(alive),
    {next_state, alive, Ld1, [
      Reply, {state_timeout, Timeout, hard_stop}]};
waiting(state_timeout, hard_stop, #session{}=Ld) ->
    {stop, waiting_timed_out, Ld};
waiting(EventType, EventContent, #session{}=Ld) ->
    handle_event({EventType, EventContent, Ld}).

%%
%% business routines
%%

handle_stop_session(waiting, _) -> ok;
handle_stop_session(_, #session{}=Ld) ->
    handle(stop_session, Ld),
    ok.

handle(start_statem) ->
    io:format(user, "*** Start statem~n", []),
    ok;
handle(stop_statem) ->
    io:format(user, "*** Stop statem~n", []),
    ok.

handle(bump_session, #session{key=Key}) -> 
    io:format(user, "*** Bump session: ~p~n", [Key]),
    ok;
handle(new_session, <<Email/bitstring>>) ->
    {ok, Key} = handle_key(),
    Ld = #session{email=Email, key=Key},
    io:format(user, "*** Create session: ~p : ~p ~n", [Email, Key]),
    {ok, Ld};
handle(start_session, {Key, #session{key=Key}=Ld}) ->
    io:format(user, "*** Start session: ~p~n", [Key]),
    {ok, Ld};
handle(stop_session, #session{key=Key}) ->
    io:format(user, "*** Stop session: ~p~n", [Key]),
    {ok, #session{}}.

handle_key() -> handle_key(8).

handle_key(N) ->
    dirty_seed(),
    Key = binary_key(N),
    {ok, Key}.

handle_event({_, _, Ld}) ->
    {keep_state, Ld}.

%%
%% support routines
%%

binary_key(N) ->
    Cs    = erlang:list_to_tuple(?CHARS),
    CsLen = erlang:tuple_size(Cs),
    Key  = [ pick(CsLen, Cs) || _ <- lists:seq(1, N) ],
    erlang:list_to_binary(Key).

dirty_seed() ->
    <<X:32, Y:32, Z:32>> = crypto:strong_rand_bytes(12),
    rand:seed(exs1024s, {X, Y, Z}).

pick(K, Ts) ->
    C = rand:uniform(K),
    erlang:element(C, Ts).

session_duration(alive) ->
    timer:hms(1, 10, 0);
session_duration(waiting) ->
    timer:hms(0, 10, 0).

Sprinkled within are some ‘print-to-screens’ to exclaim the inner workings if you want to give it a spin. After that code is compiled, and loaded, one can play with it and see how they did using the Erlang shell:

1> {ok, _} = session_sm:start_link(<"snafu@snafu.ml">).
2> {key, Key} = session_sm:request(key).
3> ok = session_sm:request(entry, Key).
4> ok = session_sm:bump().
5> ok = session_sm:request(departure).
6> init:stop().

With each, you’ll get some feedback from our code about the state transitions, and changes to the state-machine inner-loop data.

State machines

The ‘gen_statem’ seems highly capable, and the more one spends time with it, the more places one can see where it could replace boring ol’ ‘gen_server.’ Moreover, it’s exciting to see a full embrace from the OTP team for a re-implementation of one of their pillar libraries.

The days are early for ‘gen_statem’ but it’s truly titillating to finally be utilizing it at Obligd.

THIS END UPWhat’s Obligd?