Postpone: Resource Allocation on Demand

2 minute read

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.