Implementing an app using an OTP behavior and rebar3

1   Introduction

rebar3 provides a convenient and easy way to generate an Erlang OTP application that implements a supervision tree. In this post we'll walk through the steps of generating the application, adding code specific to our use case, compiling our code, testing our code under the rebar3 Erlang shell, and finally, creating and running unit tests with eunit.

2   Generate the app structure and templates

We'll call our application test02 (because, after working on test01 for awhile, it became a bit of a mess).

We generate our initial directory structure and templates as follows:

$ rebar3 new app test02

3   Add our own application-specific code

One of our central goals here is to implement a module that runs within an OTP supervision tree. That's what gets us a number of OTP benefits, importantly soft-failure, that is to say, we want a supervisor for our module that monitors the execution of that module and restarts it when it fails.

So, we'll add a module that is built on the OTP gen_server behavior. And, from that module we will export several functions, specifically hello/1 and shout/2, which merely print out messages passed to them and keep track of how many messages were printed.

Here is our sample module:

%%%-------------------------------------------------------------------
%%% @author Dave Kuhlman
%%% @copyright (C) 2016, Dave Kuhlman
%%% @doc
%%%
%%% @end
%%% Created :  1 Aug 2016 by Dave Kuhlman <dkuhlman@crow>
%%%-------------------------------------------------------------------
-module(test02_server).

-behaviour(gen_server).

%% API
-export([
     start_link/0,
     stop/0,
     hello/1,
     shout/2,
     get_count/0
    ]).

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

-define(SERVER, ?MODULE).

-record(state, {count}).

%%%===================================================================
%%% API
%%%===================================================================

%%--------------------------------------------------------------------
%% @doc
%% Starts the server
%%
%% @spec start_link() -> {ok, Pid} | ignore | {error, Error}
%% @end
%%--------------------------------------------------------------------
start_link() ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

%%--------------------------------------------------------------------
%% @doc
%% Stops the server normally.
%%
%% @spec stop() -> ok
%% @end
%%--------------------------------------------------------------------
stop() ->
    gen_server:stop(?SERVER),
    ok.

%%--------------------------------------------------------------------
%% @doc
%% Prints a message and a count.
%%
%% @spec hello(Msg :: string()) -> ok
%% @end
%%--------------------------------------------------------------------
hello(Msg) ->
    gen_server:call(?SERVER, {hello, Msg}),
    ok.

%%--------------------------------------------------------------------
%% @doc
%% Prints a message multiple times.
%%
%% @spec shout(Msg :: string(), Multiple :: integer()) -> ok
%% @end
%%--------------------------------------------------------------------
shout(Msg, Multiple) ->
    gen_server:call(?SERVER, {shout, Msg, Multiple}).

%%--------------------------------------------------------------------
%% @doc
%% Returns the count of messages printed so far.
%%
%% @spec get_count() -> {ok, Count :: integer()}
%% @end
%%--------------------------------------------------------------------
get_count() ->
    {ok, Count} = gen_server:call(?SERVER, get_count),
    {ok, Count}.

%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Initializes the server
%%
%% @spec init(Args) -> {ok, State} |
%%                     {ok, State, Timeout} |
%%                     ignore |
%%                     {stop, Reason}
%% @end
%%--------------------------------------------------------------------
init([]) ->
    {ok, #state{count=0}}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling call messages
%%
%% @spec handle_call(Request, From, State) ->
%%                                   {reply, Reply, State} |
%%                                   {reply, Reply, State, Timeout} |
%%                                   {noreply, State} |
%%                                   {noreply, State, Timeout} |
%%                                   {stop, Reason, Reply, State} |
%%                                   {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_call({hello, Msg}, _From, #state{count=Count} = State) ->
    Count1 = Count + 1,
    io:format("~B. ~s~n", [Count1, Msg]),
    State1 = State#state{count=Count1},
    Reply = ok,
    {reply, Reply, State1};
handle_call({shout, Msg, Multiple}, _From, #state{count=Count} = State) ->
    {Reply, State1} = if
        0 < Multiple ->
            {ok, Count1} = shout_loop(Msg, Count, Count + Multiple),
            {ok, State#state{count=Count1}};
        true ->
            {{error, "Must be positive integer."}, State}
    end,
    {reply, Reply, State1};
handle_call(get_count, _From, #state{count=Count} = State) ->
    Reply = {ok, Count},
    {reply, Reply, State};
handle_call(Request, _From, State) ->
    io:format("unknown request: ~p~n", [Request]),
    Reply = ok,
    {reply, Reply, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling cast messages
%%
%% @spec handle_cast(Msg, State) -> {noreply, State} |
%%                                  {noreply, State, Timeout} |
%%                                  {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_cast(_Msg, State) ->
    {noreply, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling all non call/cast messages
%%
%% @spec handle_info(Info, State) -> {noreply, State} |
%%                                   {noreply, State, Timeout} |
%%                                   {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_info(_Info, State) ->
    {noreply, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
%%
%% @spec terminate(Reason, State) -> void()
%% @end
%%--------------------------------------------------------------------
terminate(Reason, #state{count=Count} = _State) ->
    io:format("Terminated with reason ~p after ~B requests.~n",
          [Reason, Count]),
    ok.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Convert process state when code is changed
%%
%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
%% @end
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%%===================================================================
%%% Internal functions
%%%===================================================================

shout_loop(_Msg, Count, Count) ->
    {ok, Count};
shout_loop(Msg, Count, Max) ->
    Count1 = Count + 1,
    io:format("~B. ~s~n", [Count1, Msg]),
    shout_loop(Msg, Count1, Max).

Notes:

  • As you can likely recognize, I generated the initial skeleton of this module. You can use either Vim or Emacs to do that. Their OTP skeletons are almost the same. For Emacs, the Erlang support is included in the OTP release; see: http://www.erlang.org/downloads. For Vim, you can find support for OTP skeletons and more here: https://github.com/vim-erlang.
  • I added several functions to the exported API: hello/1, shout/2, and stop/0.
  • Each of these new functions makes a call to gen_server:
    • Functions hello/1 and shout/2 call gen_server:call/2, which results in a call to our callback function test02_server:handle_call/3.
    • Function get_count/0 gets the current count of the number of messages that have been processed. It illustrates how to return values from handle_call/3.
    • Function stop/0 calls gen_server:stop/1, which results in a call to our callback function test02_server:terminate/2.

4   Update the supervisor module

We need to modify the supervisor module that was generated by rebar3 so as to tell it which process (module) to monitor and how to re-start that process if and when it fails. So, we'll change the init/1 function in src/test02_sup.erl as follows:

init([]) ->
    Server = {test02_server, {test02_server, start_link, []},
          permanent, 2000, worker, [test02_server]},
    Children = [Server],
    RestartStrategy = {one_for_one, 0, 1},
    {ok, {RestartStrategy, Children}}.

Notes:

5   Run the code

Compile the code:

$ cd test02
$ rebar3 compile

Start the shell:

$ rlwrap -a -c -r rebar3 shell

I use rlwrap to give me command line history saved across sessions.

Start the application:

1> application:start(test02).

Call a function in the API:

2> test02:hello("some message").

Stop the application:

4> test02_server:stop().

Here it is all together:

1> application:start(test02).
ok
2> test02_server:hello("hi dave").
1. Hello -- "hi dave"
ok
3> test02_server:hello("hi again dave").
2. Hello -- "hi again dave"
ok
4> test02_server:get_count().
{ok,2}
5> test02_server:stop().
Terminating with reason normal after 2 messages.
ok
6>
=INFO REPORT==== 1-Aug-2016::11:55:35 ===
application: test02
exited: shutdown
type: temporary
6>

6   Eunit tests

You can learn about eunit here:

For this example I've added eunit tests in a separate module in the src/ directory. The module test02_server_tests.erl contains the tests for the code in module test02_server.erl. To run these tests we do the following:

$ rebar3 eunit --module=test02_server

Notice that we have followed the eunit module naming convention (suffix "_tests"), and because we've done so, rebar3 and eunit know to look in test02_server_tests.erl for the tests for module test02_server.erl.

And, here is our sample test module:

-module(test02_server_tests).
-include_lib("eunit/include/eunit.hrl").

hello_test() ->
    application:start(test02),
    ?assert(test02_server:hello("message one") =:= ok),
    test02_server:stop().

shout_test() ->
    application:start(test02),
    ?assert(test02_server:shout("message three", 5) =:= ok),
    {Reply1, _Reason} = test02_server:shout("message three", -4),
    ?assert(Reply1 =:= error),
    test02_server:stop().

Notes:

  • The -include_lib directive is required for eunit test modules.
  • The name of each function that we want eunit to run, should have the suffix "_test" or "_test_".
  • We test specific conditions for success and failure with the ?assert macro. Eunit provides additional macros; see: http://erlang.org/doc/apps/eunit/chapter.html#EUnit_macros.

We could, alternatively, have placed the tests in a separate directory, for example in test/test02_server_checks.erl. Had we done so, we'd run our tests with something like the following:

$ rebar3 eunit --file="test/test02_server_checks.erl"

7   An alternative strategy

With some use cases, an OTP behavior might be too heavy. For those cases, you can consider implementing a "special process". Here is an overview and description from the standard Erlang documentation:

This section describes how to write a process that complies to the OTP design principles, without using a standard behaviour. Such a process is to:

  • Be started in a way that makes the process fit into a supervision tree
  • Support the sys debug facilities
  • Take care of system messages.

Follow the instructions here: http://erlang.org/doc/design_principles/spec_proc.html (search for "Special Processes").

By following these instructions, you can implement a "server" using sys and proc_lib so it fits into a supervision tree.

links