Postpone: Resource Allocation on Demand
One of the many very nice features of statem in Erlang/OTP is the ability to postpone an event. This article will demonstrate delaying the allocation of a resource until it is demanded by postponing an event.
Let us imagine that we have an API on a resource accessed through a socket. We only want to open the socket to the resource when we need it. This is an ideal candidate for a statem using postpone.
Our API is a simple send/1
that enables us to make a request on the
resource:
send(Arg) ->
gen_statem:call(?MODULE,
{?FUNCTION_NAME, Arg}).
In the above ?MODULE is being used because this
process will be registered in the local registry using {local,
?MODULE} in start_link/4. In
addition, the ?FUNCTION_NAME will be replaced
with send
.
In our init/1 callback we have:
init([]) ->
{ok, disconnected, #{}}.
We start in the disconnected
state, with an empty map (#{}
) as our
initial data. We will transition to the connected
state once we have
opened our socket to the resource.
In general, init/1 should do the minimum work necessary. This is because start_link/4 is synchronous. It does not return until the statem is initialised and ready to receive events. Our supervisor will not start subsequent siblings until init/1 is complete.
Our callback_mode/0 is:
callback_mode() ->
handle_event_function.
In statem with
handle_event_function the main work is done
within handle_event/4. Firstly lets deal with
a request while we are in our initial disconnected
state:
handle_event({call, _}, _, disconnected, Data) ->
{next_state,
connecting,
Data,
[{next_event, internal, open}, postpone]};
In the disconnected
state, when we receive any {call,
_} request, we
transition to the connecting
state, putting
an open
as the next event, while
postponing the {call,
_} request until later.
Looking at the clauses of the connecting
state:
handle_event({call, _}, _, connecting, Data) ->
{keep_state_and_data, postpone};
handle_event(internal, open, connecting, Data) ->
case socket:open(inet, stream, default) of
{ok, Socket} ->
{keep_state,
Data#{socket => Socket},
{next_event, internal, connect}};
{error, Reason} ->
{stop, Reason}
end;
handle_event(internal,
connect,
connecting,
#{socket := Socket} = Data) ->
case socket:connect(
Socket,
#{family => inet,
port => 1234,
add => {127, 0, 0, 1}}) of
ok ->
{next_state, connected, Data};
{error, Reason} ->
{stop, Reason}
end;
In the connecting
state, when we receive a {call,
_} request, we just
postpone the request. We have already
queued an open
event in the disconnected
state. The open
event
will call socket:open/3, opening the connection to
the resource, assuming that is successful, we will then queue a
connect
event. If socket:connect/2 is ok, we
will transition to the connected
state.
Finally, the clause for the connected state:
handle_event({call, From},
{send, Msg},
connected,
#{socket := Socket}) ->
case socket:send(Socket, Msg) of
ok ->
{keep_state_and_data, {reply, From, ok}};
{error, Reason} ->
{stop, Reason}
end.
When in the connected
state we receive a {call,
_} API request, we can use
socket:send/2 to send the message to the resource and
return ok
to the caller.
Using postpone in this way is a pattern to
allocate resources only when they are required. The disconnected
state allows us to queue an open
event, whereas connecting
only
postpones the request, allowing only one connection to the
resource. In the connected
state the resource is allocated and
ready, and we can send messages as required.
statem can be quite overwhelming in terms of breadth of functionality to understand. I have found regularly reading statem behaviour, together with the OTP Design Principles User’s Guide for context. OTP can be complex to understand, but it will reward your persistence with simple elegant code.