Sync’ up! … without getting drained

may 19

Startup hook pattern

A pattern I like to use quite often when utilizing OTP’s ‘gen_server’ and ‘gen_statem’ behaviors is a hook when these processes are started.

In the case of ‘simple_one_for_one’ whereby each state-machine process, or generic server process is coming and going with some frequency, it’s often the case that I want to initialize the services, then let it take care of sorting this and that for it to be useful.

For example, if I have a server that needs resources to truly be in a ready state, what I will do is start it up, and have itself handle getting its things in order, rather than have the calling process hang around telling it what it needs after it starts.

This pattern isn’t seen in the wild, too much, so I thought I’d share a couple flavors of it.

In the simplest case, using ‘gen_server,’ we have ‘init/1’ return quickly, as is often the heuristic, then for the return value, we have the tuple {ok, #ld{}, 0} where ‘0’ is a timeout of immediate. The timeout is handled in the ‘handle_info/2’ routine where we can place all our duty code to get the server or state-machine gathering all that it needs to be happy. Here’s the above code in an Erlang module called ‘hook_srv.erl’ that does all this:

-module(hook_srv).
-behavior(gen_server).

%% api
-export([start_link/0]).

%% callback
-export([
  init/1,
  handle_call/3,
  handle_cast/2,
  handle_info/2,
  terminate/2,
  code_change/3]).

-define(REGISTERED, ?MODULE).

-record(ld, {}).

%%
%% api routines
%%

start_link() ->
    gen_server:start_link({local, ?REGISTERED}, ?MODULE, [], []).

%%
%% callback routines
%%

init([]) ->
    {ok, #ld{}, 0}.

handle_call(Request, _From, Ld) ->
    error_logger:info_report([{unknown_call, Request}]),
    {reply, ignored, Ld}.

handle_cast(Msg, Ld) ->
    error_logger:info_report([{unknown_cast, Msg}]),
    {noreply, Ld}.

handle_info(timeout, Ld) ->
    error_logger:info_report([{hook_called, ok}]),
    {noreply, Ld};
handle_info(_Info, Ld) ->
    {noreply, Ld}.

terminate(_Reason, _Ld) ->
    ok.

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

You can compile it and and try it out in the Erlang shell as follows:

1> c(hook_srv).
2> {ok, _} = hook_srv:start_link().

You can see that the ‘info_report’ is created instantly the server starts.

In the case where this pattern is needed, but we are utilizing ‘gen_statem’ not ‘gen_server,’ the following has the same effect:

-module(hook_sm).
-behavior(gen_statem).

%% api
-export([
  start_link/0,
  push/0,
  get_count/0,
  stop/0]).

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

%% state-callback
-export([
  on/3,
  off/3]).

-define(REGISTERED, ?MODULE).

%%
%% api routines
%%

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

push() ->
    gen_statem:call(?REGISTERED, push).

get_count() ->
    gen_statem:call(?REGISTERED, get_count).

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

%%
%% callback routines
%%

init([]) ->
    [ State | Ld ] = [ off | 0 ],
    {ok, State, Ld, 0}.

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

callback_mode() -> state_functions.

terminate(_Reason, _State, _Ld) ->
    void.

%%
%% state-callback routines
%%

off({call, From}, push, Ld) ->
    % Go to 'on,' increment count and reply that 
    % the resulting status is 'on'
    Ld1 = Ld + 1,
    {next_state, on, Ld1, [{reply, From, on}]};
off(EventType, EventRequest, Ld) ->
    handle_event(EventType, EventRequest, Ld).

on({call, From}, push, Ld) ->
    % Go to 'off' and reply that the resulting 
    % status is 'off'
    {next_state, off, Ld, [{reply, From, off}]};
on(EventType, EventRequest, Ld) ->
    handle_event(EventType, EventRequest, Ld).

%%
%% business routines
%%

handle_event({call, From}, get_count, Ld) ->
    % Reply with the current count
    {keep_state, Ld, [{reply, From, Ld}]};
handle_event(timeout, 0, _Ld) ->
    error_logger:info_report([{hook_called, ok}]),
    keep_state_and_data;
handle_event(Type, Request, Ld) ->
    % Ignore all other events
    error_logger:info_report([
      {unknown_statem_event, [{type, Type}, {request, Request}]}]),
    {keep_state, Ld}.

Here, since we are using the ‘state_functions’ as the callback-mode, the timeout pattern match falls through to our user-defined ‘handle_event/3’ routine.

The above Erlang shell commands will work here, too, save for the namespace, where you will need to use ‘hook_sm’ for ‘hook_srv.’

This startup hook pattern is plenty useful, but doesn’t come up in books or guides often. Using it can clean up your code and the pattern is sound as it allows processes to get started, then queue further requests as it handles its internal work concurrently.

Update May. 2020

Although there is no handle_continue/2 callback routine for ‘gen_statem,’ there is one for ‘gen_server’ that can make the timeout solution more elegant as of OTP 21: erlang.org/doc/man/gen_server.html