Erlang Central

Difference between revisions of "Building An OTP Application"

From ErlangCentral Wiki

(Part Two: Crafting a Release)
(38 intermediate revisions by 3 users not shown)
Line 4:Line 4:
 
== Building an OTP Application Overview ==
 
== Building an OTP Application Overview ==
  
Erlang is a clean and simple language to learn.  The learning curve goes up significantly when we start to talk about OTP.  How to start an application, what the function of a supervisor is, where to put the gen_server. That is just the beginning it gets far more confusing when questions are asked like, "what is a .app file, what is a release, and can someone please tell me what .script and .boot files do?"
+
Erlang is a clean, simple language, and it is easy to learn.  That's Erlang, now lets talk OTP.  The learning curve goes up significantly when getting into OTP.  Questions arise, such as; how do I start an application, what is the function of a supervisor, and how do I make use of gen_server? That is just the beginning it gets far more confusing... "what is a .app file, what is a release, and can someone please tell me what .script and .boot files do?"
  
This tutorial will walk you through creating a new OTP application called location_server. This application will store the location of clients, allow others to query for that location from any node in our network, and allow clients to subscribe to notifications in the event of a change of location. Over the course of creating the location_server I will answer all the above questions.  
+
This tutorial will walk you through creating a new OTP application called location_server. This application will store the location of clients, allow others to query for that location from connected node, and allow clients to subscribe to notifications that are sent in the event of a change of location. Over the course of creating the location_server I will answer all of the above questions on OTP applications and quite a few more besides.
  
 
== Part One: Building the Server ==
 
== Part One: Building the Server ==
  
First thing to do is download the reference build system for this tutorial, this can be found at <b>[http://www.erlang.org www.erlware.org]</b> under <b>downloads otp_base-&lt;vsn&gt;</b> Once you have the code on your local Unix/Linux machine extract the build system.
+
The following instructions may be inaccurate, as otp_base has moved to google code: [http://code.google.com/p/otp-base/ http://code.google.com/p/otp-base/].
 +
 
 +
You will need to download the reference build system for this tutorial, this can be found at <b>[http://www.erlware.org www.erlware.org]</b> under <b>downloads otp_base-&lt;vsn&gt;</b> Once you have the code on your local Unix/Linux machine extract the build system.
  
 
<code caption="Code listing 1.0">
 
<code caption="Code listing 1.0">
Line 19:Line 21:
 
</code>
 
</code>
  
The first thing we are going to do is make an initial build of our build system support files, this is done by typing "make" in the otp directory.
+
The first thing we are going to do is make an initial build of our build system, this is done by typing "make" in the otp directory. Note: if you have Erlang installed in any place other than /usr/local/lib/erlang then you can either create a symlink there pointing to your install, or you can set the environment variable ERL_RUN_TOP to point to your installation (make sure you use an absolute path). 
  
The second thing we are going to do is create a skeleton for our application.  Once we do I will disect that skeleton and explain the meaining of all its parts.  To create our location_server application we use the "appgen" utility.
+
The second thing to do is create a skeleton for the application.  Once we do I will dissect that skeleton and explain the meaning of all its parts.  To create the location_server application skeleton we use the "appgen" utility.
  
 
<code caption="Code listing 1.1">
 
<code caption="Code listing 1.1">
Line 29:Line 31:
 
</code>
 
</code>
  
We have just created an application under lib/location_server and a release under release/location_server_rel.  We will now cd into lib/location server and see what we have made.
+
We have just created an application under lib/location_server and a release under release/location_server_rel.  We will now cd into lib/location_server and see what we have made.
  
 
<code caption="Code listing 1.2">
 
<code caption="Code listing 1.2">
Line 37:Line 39:
 
</code>
 
</code>
  
The include directory contains a file called location_service.hrl.  This is the place we will put all our shared macro and record definitions.  The makefile does what you would expect it to.  The src directory contains all of our erlang (.erl) files. The vsn.mk file contains the make variable LOCATION_SERVER_VSN=1.0 which is the version of our application.  This version plays a role in how we handle releases.  
+
The include directory contains a file called location_service.hrl.  This is the place we will put all our shared macro and record definitions.  The src directory contains all of our Erlang (.erl) files. The vsn.mk file contains the make variable LOCATION_SERVER_VSN=1.0 which is the initial version of our application (feel free to change this).  This version plays a role in how releases are handled, more on this later.
  
 
<code caption="Code listing 1.3">
 
<code caption="Code listing 1.3">
 
&gt; cd src
 
&gt; cd src
 
&gt; ls
 
&gt; ls
&gt;
 
 
location_server.app.src    location_server.erl  ls_sup.erl
 
location_server.app.src    location_server.erl  ls_sup.erl
 
location_server.appup.src  ls_server.erl        Makefile
 
location_server.appup.src  ls_server.erl        Makefile
</code>  
+
</code>
  
Our src directory contains a file called location_server.erl.  Notice that this Erlang source file has the same name as our application.  By convention this file contains the API for our application.  This file also contains the -behaviour(application) directive.  This means that this file is the entry point for the OTP application structure.
+
The src directory contains a file called location_server.erl.  Notice that this Erlang source file has the same name as our application.  By convention this file contains the API for our application.  This file also contains the -behaviour(application) directive.  This means that this file can be used as the entry point for the OTP application structure.
  
 
<code caption="Code listing 1.4">
 
<code caption="Code listing 1.4">
Line 94:Line 95:
 
%%--------------------------------------------------------------------
 
%%--------------------------------------------------------------------
 
shutdown() ->
 
shutdown() ->
     application:stop(auction_server).
+
     application:stop(location_server).
  
 
%%====================================================================
 
%%====================================================================
Line 101:Line 102:
  
 
%%--------------------------------------------------------------------
 
%%--------------------------------------------------------------------
%% Called upon the termintion of an application.
+
%% Called upon the termination of an application.
 
%%--------------------------------------------------------------------
 
%%--------------------------------------------------------------------
 
stop(State) ->
 
stop(State) ->
Line 107:Line 108:
 
</code>
 
</code>
  
The start/2 function is a callback function used by the OTP application system to start applications specified by the OTP release system.  This function must take two arguments, I will get to what those are later on, and it must return {ok, Pid} for a succesful application startup. Notice that the start/2 function calls ls_sup:start_link/1. ls_sup.erl contains our top level supervisor. Notice the prefix ls it is the first two letters of the two words that make up our application name; location_server.  This prefix is a convention and it is used in liu of a package structure in Erlang to avoid name conflicts within a release.  
+
The start/2 function is a callback function used by the OTP application system to start applications specified by the OTP release system.  This function must take two arguments (you can read the docs on supervision for more info on the args) and it must return {ok, Pid} or {ok, Pid, State}, the former being more typical, for a successful application startup. Notice that the start/2 function calls ls_sup:start_link/1. ls_sup.erl contains our top level supervisor. Notice the prefix "ls" in the name ls_sup it is the first two letters of the two words that make up our application name; location_server.  This prefix is a convention and it is used in lieu of a package structure in Erlang to avoid name conflicts within a release. You can use anything for your prefix, I only offer the first letter of each word in your application name as a suggestion.
  
The top level supervisor starts all the workers and lower level supervisors that our application will require.  It specifies a strategy detailing how processes will be restarted when they crash.  This a key concept in Erlang and I suggest reading the design principles on supervision located at www.erlang.org. Ok, lets take a look at our supervisor.
+
The top level supervisor starts all the workers and lower level supervisors that our application will require.  It specifies a strategy detailing how its processes will be restarted when they crash.  This a key concept in Erlang and I suggest reading the design principles on supervision located at www.erlang.org.
  
 
<code caption="Code listing 1.5">
 
<code caption="Code listing 1.5">
Line 167:Line 168:
 
     {ls_server,
 
     {ls_server,
 
       {ls_server, start_link, []},
 
       {ls_server, start_link, []},
       transient,
+
       permanent,
 
       1000,
 
       1000,
 
       worker,
 
       worker,
Line 176:Line 177:
 
</code>
 
</code>
  
This file exports two functions start_link/1 and init/1. These are both callbacks that conform to the supervisor behaviour, note the -behaviour(supervisor) directive which is present at the top of the file. Naming a function start_link is a convention in OTP that means the function will spawn a process and that the caller of the function, the parent process, will be linked (see link/1) to the child process.  In this case the OTP application structure will be linked to the supervisor process.  
+
This file exports two functions start_link/1 and init/1. These are both functions used to conform to the supervisor behaviour, note the -behaviour(supervisor) directive which is present at the top of the file. Naming a function start_link is a convention in OTP that means the function will spawn a process and that the caller of the function, the parent process, will be linked (see link/1) to the child process.  In this case the OTP application structure will be linked to the supervisor process.
  
The init/1 function is where the true guts of the supervisor are. Remember that you should never put any program logic in your supervisor, it should be as simple as possible and never fail, the fate of your application depends on this.  Lets break this down:
+
The init/1 function is where the true guts of the supervisor are. Remember that you should never put any program logic in your supervisor, it should be as simple as possible and never fail, the fate of your application depends on this.  Lets break our super down:
  
<code caption="Code listing 1.6">  
+
<code caption="Code listing 1.6">
 
RestartStrategy    = one_for_one,
 
RestartStrategy    = one_for_one,
 
MaxRestarts        = 1000,
 
MaxRestarts        = 1000,
Line 186:Line 187:
 
</code>
 
</code>
  
The RestartStrategy one_for_one means that for each process that dies simply restart that process without affecting any of the others.  There are two other restart strategies, one_for_all and rest_for_one, you can read about these in the supervisor documentation. MaxRestarts is the maxumum number of restarts allowed within MaxTimeBetweenRestarts seconds.
+
The RestartStrategy one_for_one means that for each process that dies simply restart that process without affecting any of the others.  There are two other restart strategies, one_for_all and rest_for_one, you can read about these in the supervisor documentation. MaxRestarts is the maximum number of restarts allowed within MaxTimeBetweenRestarts seconds.
  
 
<code caption="Code listing 1.7">
 
<code caption="Code listing 1.7">
Line 197:Line 198:
 
</code>
 
</code>
  
This supervisor has one ChildSpec which means it will start exactly one process. Going in order down the data structure ls_server is the name of the process to be started, {ls_server, start_link, []} is the module function and args that is called to start the child, permanent refers to the fact that the process should never be alowed to die without being restarted, 1000 is the time the process should be given to shutdown before being brutally killed, worker means the child is not a supervisor, [ls_server] is a list of module dependencies of the child.
+
This supervisor has one ChildSpec which means it will start exactly one process. Going in order down the data structure ls_server is the name of the process to be started, {ls_server, start_link, []} is the module function and args that is called to start the child, permanent refers to the fact that the process should never be allowed to die without being restarted (there are other options, see the docs on supervision at erlang.org), 1000 is the time the process should be given to shutdown before being brutally killed, worker means the child is not a supervisor, [ls_server] is a list of module dependencies of the child.
  
The supervisor callback module function init/1 returns all of this information and the supervisor behaviour takes care of starting all the children in the order that they were specified in the child specs.  
+
The supervisor callback module function init/1 returns all of this information and the supervisor behaviour takes care of starting all the children in the order that they were specified in the child specs.
  
 
Now we move on to the ls_server.erl file.  This file exhibits the behaviour gen_server. This means that it follows the callback prescription for that behaviour and exports the following callback functions:
 
Now we move on to the ls_server.erl file.  This file exhibits the behaviour gen_server. This means that it follows the callback prescription for that behaviour and exports the following callback functions:
Line 225:Line 226:
 
</code>
 
</code>
  
Lets get started. It is important to document all of your external functions.  The build system supports "edoc" with the "make docs" target which will automatically go through and compile all of your documentation and place it in your application directory under the directory doc. And now on to our functions:
+
Lets get started. It is important to document all of your external functions.  The OTP Base build system supports "edoc" with the "make docs" target which will automatically go through and compile all of your documentation and place it in your application directory under the directory doc (location_server/doc). On to our functions:
  
 
<code caption="Code listing 1.10">
 
<code caption="Code listing 1.10">
Line 237:Line 238:
 
</code>
 
</code>
  
This function starts the server, registers it locally as ?SERVER (see source for -define(SERVER, ?MODULE).) and indicates to the gen_server behaviour that the current module exports the needed callback functions.  
+
This function starts the server, registers it locally as ?SERVER (see source for -define(SERVER, ?MODULE).) and indicates to the gen_server behaviour that the current module exports the needed callback functions.
  
 
<code caption="Code listing 1.11">
 
<code caption="Code listing 1.11">
Line 249:Line 250:
 
</code>
 
</code>
  
Stop is simple, but notice the use of gen_server:cast/2.  This function is about the same as using the "!" operator.  It is used to send an asynchronous message to a gen server.  In this case we send the atom 'stop' to a locally registered process identified by the macro ?SERVER.  
+
Stop is simple, but notice the use of gen_server:cast/2.  This function is about the same as using the "!" operator.  It is used to send an asynchronous message to a gen server.  In this case we send the atom 'stop' to a locally registered process identified by the macro ?SERVER.
  
<code caption="Code listing 1.11">
+
<code caption="Code listing 1.12">
 
%%--------------------------------------------------------------------
 
%%--------------------------------------------------------------------
 
%% @doc Stores the location of a client.
 
%% @doc Stores the location of a client.
%% &lt;pre&gt;  
+
%% &lt;pre&gt;
 
%% Types:
 
%% Types:
 
%%  Node = node()
 
%%  Node = node()
Line 267:Line 268:
 
</code>
 
</code>
  
store_location/3 is a very simple function but there is a lot for us to pay attention to here.  Notice that we use the guard expressions, is_atom/1, and is_list/1.  We are making sure that the types of the variables passed in are correct.  This is an example of not trusting our borders.  We check the types here so that we will never again have to check them.  Notice that the name of the function is the same as the "tag" on the message we are sending:
+
store_location/3 is a very simple function but there is a lot for us to pay attention to here.  Notice that we use the guard expressions, is_atom/1, and is_list/1.  We are making sure that the types of the variables passed in are correct.  This is an example of not trusting our borders.  We check the types here so that we will never again have to check them.  Did you notice that the name of the function is the same as the "tag" on the message we are sending?  No? I will explain;
  
<code caption="Code listing 1.12">
+
<code caption="Code listing 1.13">
 
gen_server:cast({?SERVER, Node}, {store_location, {Id, Location}}).
 
gen_server:cast({?SERVER, Node}, {store_location, {Id, Location}}).
 
</code>
 
</code>
  
store_location in that line is our tag and will differentiate this message from others that this server understands. {Id, Location} is the payload that we are sending with the tag store_location. Messages sent in the gen_server should follow this convention of {Tag, Value}.  There is an even more important thing to notice about the line above. Notice that the first argument to this function is {?SERVER, Node} instead of just ?SERVER as we saw in cast.  This is the case because we want to be able to contact a process on any node in the network not just our own.  To message a remotely registered process we can use the syntax {RegisteredName, Node} where node can be any node in the network including the local node.
+
store_location in the line above is our "tag" and will differentiate this message from others that this server understands (its alphabet). {Id, Location} is the payload that we are sending with the tag store_location. Messages sent in the gen_server should follow this convention of {Tag, Value}.  There is an even more important thing to notice about the line above. Notice that the first argument to this function is {?SERVER, Node} instead of just ?SERVER as we saw in cast.  This is the case because we want to be able to contact a process on any node in the network not just our own.  To message a remotely registered process we can use the syntax {RegisteredName, Node} where node can be any node in the network including the local node.
  
<code caption="Code listing 1.13">
+
<code caption="Code listing 1.14">
 
%%--------------------------------------------------------------------
 
%%--------------------------------------------------------------------
 
%% @doc Fetches the location of a client by its Id.
 
%% @doc Fetches the location of a client by its Id.
%% &lt;pre&gt;  
+
%% &lt;pre&gt;
 
%% Types:
 
%% Types:
 
%%  Node = node()
 
%%  Node = node()
Line 290:Line 291:
 
</code>
 
</code>
  
The things to notice here is that we no longer use guards, we don't care, this function will not change the state of our server and if it fails it does not matter much to us. Most importantly notice the use of gen_server:call/2.  This function makes a blocking synchronous call to a gen_server.  
+
The things to notice here is that we no longer use guards, we don't care, this function will not change the state of our server and if it fails it does not matter much to us. Most importantly notice the use of gen_server:call/2.  This function makes a synchronous (blocking) call to a gen_server.
  
<code caption="Code listing 1.14">
+
<code caption="Code listing 1.15">
 
%%--------------------------------------------------------------------
 
%%--------------------------------------------------------------------
%% @doc Subscribes the caller to notifications of location change for  
+
%% @doc Subscribes the caller to notifications of location change for
 
%% the given Id. When a location changes the caller of this function
 
%% the given Id. When a location changes the caller of this function
 
%% will be sent a message of the form {location_changed, {Id, NewLocation}}.
 
%% will be sent a message of the form {location_changed, {Id, NewLocation}}.
%% &lt;pre&gt;  
+
%% &lt;pre&gt;
 
%% Types:
 
%% Types:
 
%%  Node = node()
 
%%  Node = node()
Line 309:Line 310:
 
</code>
 
</code>
  
Now that we have all of our external functions finished lets flesh out our server.  One thing that those new to Erlang often get confused by is the idea that these external functions are called in the process space of the calling process but that the messages they generate are received by clauses in the same module but that exist in an entirely different process. It is important to use modules to encapsulate server protocols when possible.  
+
Now that we have all of our external functions finished lets flesh out our server.  One thing that those new to Erlang often get confused by is the idea that these external functions are called in the process space of the calling process but that the messages they generate are received by clauses in the same module but that exist in an entirely different process. It is important to use modules to encapsulate server protocols when possible.
  
<code caption="Code listing 1.15">
+
<code caption="Code listing 1.16">
 
%%====================================================================
 
%%====================================================================
 
%% Server functions
 
%% Server functions
Line 331:Line 332:
 
The init function is used to setup process state before the process can be accessed from the outside.  Here our state is embodied by a [http://www.erlang.org/doc/doc-5.5.1/lib/stdlib-1.14.1/doc/html/index.html dict]. This dict will be used to store key value pairs of the form {Id, {Location, ListOfSubscribers}}. The init function is also logging the fact that the ls_server process is starting up. The location and management of log files will be covered in the second part of this tutorial covering how to create a release for your application.
 
The init function is used to setup process state before the process can be accessed from the outside.  Here our state is embodied by a [http://www.erlang.org/doc/doc-5.5.1/lib/stdlib-1.14.1/doc/html/index.html dict]. This dict will be used to store key value pairs of the form {Id, {Location, ListOfSubscribers}}. The init function is also logging the fact that the ls_server process is starting up. The location and management of log files will be covered in the second part of this tutorial covering how to create a release for your application.
  
<code caption="Code listing 1.16">
+
<code caption="Code listing 1.17">
 
%%--------------------------------------------------------------------
 
%%--------------------------------------------------------------------
 
%% Function: handle_call/3
 
%% Function: handle_call/3
Line 353:Line 354:
 
             NewState            = dict:store(Id, {Location, NewListOfSubscribers}, State),
 
             NewState            = dict:store(Id, {Location, NewListOfSubscribers}, State),
 
             {reply, true, NewState};
 
             {reply, true, NewState};
         error ->  
+
         error ->
 
             {reply, false, State}
 
             {reply, false, State}
 
     end.
 
     end.
 
</code>
 
</code>
  
<code caption="Code listing 1.17">
+
<code caption="Code listing 1.18">
 
%%--------------------------------------------------------------------
 
%%--------------------------------------------------------------------
 
%% Function: handle_cast/2
 
%% Function: handle_cast/2
Line 368:Line 369:
 
handle_cast(stop, State) ->
 
handle_cast(stop, State) ->
 
     {stop, normal, State};
 
     {stop, normal, State};
handle_cast({store_location, Id, Location}, State) ->
+
handle_cast({store_location, {Id, Location}}, State) -> % Remember that State is a dict()
     case dict:find(Id, State) of
+
     NewState =
        {ok, {Location, ListOfSubscribers}} ->
+
        case dict:find(Id, State) of
            lists:foreach(fun(Subscriber) -> Subscriber ! {location_changed, {Id, Location}} end, ListOfSubscribers),
+
            {ok, {OldLocation, ListOfSubscribers}} ->
            {reply, true, State};
+
                lists:foreach(fun(Subscriber) -> Subscriber ! {location_changed, {Id, Location}} end, ListOfSubscribers),
        error ->  
+
                dict:store(Id, {Location, ListOfSubscribers}, State);
            NewState = dict:store(Id, {Location, []}, State),
+
            error ->
            {reply, false, NewState}
+
                dict:store(Id, {Location, []}, State)
    end,
+
        end,
     {noreply, State}.
+
     {noreply, NewState}.
 
</code>
 
</code>
  
Ok, that is it, we are done with the server all we have to do is change directories into our application directory, type "make" and "make docs", and wait for a succesful source and documentation compile.
+
We are done with the server and all the functionality of our location_server application, but we have one last step to take.  It is clearly awkward and ugly to have clients of our application calling ls_server:store_location etc... What we need is a well defined external API for clients.  The place to put that API is in the module that has the same name as our application, our application behaviour module, location_server.erl.
  
<code caption="Code listing 1.18">
+
<code caption="Code listing 1.19">
 +
%%--------------------------------------------------------------------
 +
%% @doc Stores the location of a client.
 +
%% &lt;pre&gt;
 +
%% Types:
 +
%%  Node = node()
 +
%%  Id = atom()
 +
%%  Location = string()
 +
%% &lt;/pre&gt;
 +
%% @spec store_location(Node, Id, Location) -> ok
 +
%% @end
 +
%%--------------------------------------------------------------------
 +
store_location(Node, Id, Location) ->
 +
    ls_server:store_location(Node, Id, Location).
 +
 
 +
%%--------------------------------------------------------------------
 +
%% @doc Fetches the location of a client by its Id.
 +
%% &lt;pre&gt;
 +
%% Types:
 +
%%  Node = node()
 +
%%  Id = atom()
 +
%% &lt;/pre&gt;
 +
%% @spec fetch_location(Node, Id) -> {ok, Location} | {error, Reason} | EXIT
 +
%% @end
 +
%%--------------------------------------------------------------------
 +
fetch_location(Node, Id) ->
 +
    ls_server:fetch_location(Node, Id).
 +
 
 +
%%--------------------------------------------------------------------
 +
%% @doc Subscribes the caller to notifications of location change for
 +
%% the given Id. When a location changes the caller of this function
 +
%% will be sent a message of the form {location_changed, {Id, NewLocation}}.
 +
%% &lt;pre&gt;
 +
%% Types:
 +
%%  Node = node()
 +
%%  Id = atom()
 +
%% &lt;/pre&gt;
 +
%% @spec alert_to_change(Node, Id) -> bool() | EXIT
 +
%% @end
 +
%%--------------------------------------------------------------------
 +
alert_to_change(Node, Id) ->
 +
    ls_server:alert_to_change(Node, Id).
 +
</code>
 +
 
 +
Of course the functions must be exported.
 +
 
 +
<code caption="Code listing 1.20">
 +
%%--------------------------------------------------------------------
 +
%% External exports
 +
%%--------------------------------------------------------------------
 +
-export([
 +
      start/2,
 +
      shutdown/0,
 +
      store_location/3,
 +
      fetch_location/2,
 +
      alert_to_change/2,
 +
      stop/1
 +
    ]).
 +
</code>
 +
 
 +
Now we are done with the coding of location_server, all we have to do is change directories into our application directory, type "make" and "make docs", and wait for a successful source and documentation compile.
 +
 
 +
<code caption="Code listing 1.21">
 
&gt; cd ..
 
&gt; cd ..
 
&gt; make
 
&gt; make
Line 392:Line 455:
 
Notice that there are two new directories in our application root directory "doc" and "ebin". location_server/doc contains html documentation of everything we have done.  All of our external functions are present in these docs. Lets explore the ebin directly a little more deeply.
 
Notice that there are two new directories in our application root directory "doc" and "ebin". location_server/doc contains html documentation of everything we have done.  All of our external functions are present in these docs. Lets explore the ebin directly a little more deeply.
  
<code caption="Code listing 1.19">
+
<code caption="Code listing 1.22">
 
&gt; cd ebin
 
&gt; cd ebin
 
&gt; ls
 
&gt; ls
Line 399:Line 462:
 
</code>
 
</code>
  
The ebin contains a single .beam bytecode compiled file for each of our .erl source files.  The ebin directory also contains a .app and a .appup file. The .appup file is used for release upgrade handling and is beyond the scope of this tutorial.  The .app file is essential for creating and installing OTP releases so we will look at it in depth.
+
The ebin contains a single .beam byte-code compiled file for each of our .erl source files.  The ebin directory also contains a .app and a .appup file. The .appup file is used for release upgrade handling and is beyond the scope of this tutorial.  The .app file is essential for creating and installing OTP releases so we will look at it in depth.
  
<code caption="Code listing 1.20">
+
<code caption="Code listing 1.23">
 
{application, location_server,
 
{application, location_server,
 
  [
 
  [
Line 407:Line 470:
 
   {description, "An Erlang Application."},
 
   {description, "An Erlang Application."},
  
   % The version of the applicaton
+
   % The version of the application
 
   {vsn, "1.0"},
 
   {vsn, "1.0"},
  
Line 449:Line 512:
 
== Part Two: Crafting a Release ==
 
== Part Two: Crafting a Release ==
  
The first thing we need to do is get into the release skeleton that the tools/utilities/appgen script has created for us. From the otp directory:
+
From the otp directory:
 
+
  
 
<code caption="Code listing 2.0">
 
<code caption="Code listing 2.0">
Line 459:Line 521:
 
</code>
 
</code>
  
This directory contains a vsn.mk file which serves to store the version string for this release.  It contains a yaws.conf.src file which will be used to generate a yaws.conf file should you choose to include the yaws webserver in your release. The two files we are going to concentrate on right now are the location_server_rel.config.src file and the location_server.rel.src file. The .config.src file is what make will translate into a .config file.  This file includes all of the configuration the different applications in our release require to do things such as truncate and rotate our log files. The .rel.src file is what the make process will turn into a .rel file which will inturn be used to create our .script and .boot files, the files that tell the Erlang system what code to load when starting up.  Here is our .rel.src file.
+
This directory contains a vsn.mk file which serves to store the version string for this release.  It contains a yaws.conf.src file which will be used to generate a yaws.conf file should you choose to include the yaws web server in your release. The two files we are going to concentrate on right now are the location_server_rel.config.src file and the location_server.rel.src file. The .config.src file is what make will translate into a .config file.  This file includes all of the configuration the different applications in our release require to do things such as truncate and rotate our log files. The .rel.src file is what the make process will turn into a .rel file which will inturn be used to create our .script and .boot files, the files that tell the Erlang system what code to load when starting up.  Here is our .rel.src file.
  
 
<code caption="Code listing 2.1">
 
<code caption="Code listing 2.1">
Line 476:Line 538:
 
</code>
 
</code>
  
This file specifies the name of our release followed by a version string that will be filled in when we do a make with the version string present in our vsn.mk file. Following that we specify that we want erts to be present in our release and the kernel, stdlib, sasl, fslib, gas, and finally our own custom application the location_server. For more details on fslib and gas, the two applications that together provide configuration and log rotation and truncation for our release you can go to [http://www.erlware.org erlware.org] and see full documentation about them.  Alternatively you can cd to either otp/lib/fslib or otp/lib/gas and run "make docs" and just generate all of the docs locally.  When we run make this rel.src file will be turned into an actual .rel file which contains all the version numbers associated with the applications in the .rel.src file.  For more info on the .rel.src structure take a look at the docs for the fslib file [http://www.erlware.org/fos_erlware/markup/fslib_doc/index.html fs_boot_smithe].  
+
This file specifies the name of our release followed by a version string that will be filled in when we do a make with the version string present in our vsn.mk file. Following that we specify that we want erts to be present in our release and the kernel, stdlib, sasl, fslib, gas, and finally our own custom application the location_server. For more details on fslib and gas, the two applications that together provide configuration and log rotation and truncation for our release you can go to [http://www.erlware.org erlware.org] and see full documentation about them.  Alternatively you can cd to either otp/lib/fslib or otp/lib/gas and run "make docs" and just generate all of the docs locally.  When we run make this rel.src file will be turned into an actual .rel file which contains all the version numbers associated with the applications in the .rel.src file.  For more info on the .rel.src structure take a look at the docs for the fslib file [http://www.erlware.org/fos_erlware/markup/fslib_doc/index.html fs_boot_smithe].
  
Time to build and run our release.  
+
Time to build and run our release.
  
 
<code caption="Code listing 2.2">
 
<code caption="Code listing 2.2">
Line 504:Line 566:
 
Eshell V5.4.12  (abort with ^G)
 
Eshell V5.4.12  (abort with ^G)
 
(martinjlogan_location_server_rel@core.martinjlogan.com)1&gt;
 
(martinjlogan_location_server_rel@core.martinjlogan.com)1&gt;
</code>
 
 
 
We have successfully started the location_server release. Can you see where the log message we placed in ls_server to indicate its having reached the init/1 function has shown up on the console?  That is not the only place our message shows up:
 
We have successfully started the location_server release. Can you see where the log message we placed in ls_server to indicate its having reached the init/1 function has shown up on the console?  That is not the only place our message shows up:
 
  
 
<code caption="Code listing 2.4">
 
<code caption="Code listing 2.4">
Line 514:Line 573:
 
</code>
 
</code>
  
The err_log file contains all the logs that get printed via the sasl error_logger with info_msg or error_msg.  The other log, sasl_log, is where you will find all the sasl error reports.  If your release crashes the first place to look is the sasl_log.  By default the build system has wired in the use of fs_elwrap_h via the G.A.S application that is part of your release in order to truncate and rotate logfiles.  This prevents the log files from growing indefinitely.  To configure or remove this functionality you can use the location_server.config file located in the "local" directory.  
+
The err_log file contains all the logs that get printed via the sasl error_logger with info_msg or error_msg.  The other log, sasl_log, is where you will find all the sasl error reports.  If your release crashes the first place to look is the sasl_log.  By default the build system has wired in the use of fs_elwrap_h via the G.A.S application that is part of your release in order to truncate and rotate log-files.  This prevents the log files from growing indefinitely.  To configure or remove this functionality you can use the location_server.config file located in the "local" directory.
  
  
Line 531:Line 590:
  
 
The mod_specs inform the GAS application as to how and what services to start up for a release. Following that are the actual config tuples elwrap uses to do its job. Starting with the location of the err_log and followed by the wrap specs which indicate for both the sasl and error logs how large a file can get before it is rotated out and just how many files there may be for a given logger before old files start getting deleted. The last bit of config toggles whether or not the error logger writes to the screen.
 
The mod_specs inform the GAS application as to how and what services to start up for a release. Following that are the actual config tuples elwrap uses to do its job. Starting with the location of the err_log and followed by the wrap specs which indicate for both the sasl and error logs how large a file can get before it is rotated out and just how many files there may be for a given logger before old files start getting deleted. The last bit of config toggles whether or not the error logger writes to the screen.
 +
 +
== Part Three: Testing What We Have Built ==
 +
 +
To test our project we are going to start 3 separate Erlang named nodes.
 +
Open three terminal windows and start three nodes
 +
# is the location server as started by '''local/location_server.sh'''
 +
# is started with '''erl -name a -pz &lt;path to location_server ebin&gt;'''
 +
# is started with '''erl -name b -pz &lt;path to location_server ebin&gt;'''
 +
The '''-pz''' option on both tells the interpreter to add the specified path to the code loader search path. This means that we will have the location_server interfaces available for our use in node '''a''' and '''b'''. (This loading of code without starting an application can be accomplished by adding the tuple 'none' after an included application in the .rel.src file.) So now we have three nodes '''a@hostname''', '''b@hostname''', and '''username_location_server@hostname'''. To connect our nodes execute '''net_adm:ping('a@hostname')''' from the other two nodes.  After this is complete running '''nodes()''' on any of the nodes should yield a complete list of the other nodes in our cloud. Run the following commands on your nodes. Make sure you pay attention to the node names in the following example because the commands are not all run on the same node.
 +
 +
<code caption="Code listing 3.0">
 +
(a@hostname)1> location_server:store_location('username_location_server_rel@hostname', john, "at work").
 +
ok
 +
 +
(b@hostname)1> location_server:alert_to_change('username_location_server_rel@hostname', john).
 +
true
 +
(b@hostname)2> receive Msg -> io:format("Msg: ~p~n", [Msg]) end.
 +
 +
(a@hostname)2> location_server:store_location('username_location_server_rel@hostname', john, "at home").
 +
ok
 +
 +
(b@hostname)2>
 +
Msg: {location_changed,{john,"at home"}}
 +
ok
 +
 +
(a@hostname)3> location_server:fetch_location('username_location_server_rel@hostname', john).
 +
{ok,"at home"}
 +
</code>
 +
 +
Here is what happened above, we started a location server as the node ''''username_location_server_rel@hostname'''' via the location_server_rel.sh start-up script.  Then we started two nodes '''a@hostname''' and '''b@hostname'''. We then followed these steps:
 +
<ol>
 +
<li>At prompt count 1 on '''"a"''' we did a store location with the key ''''john'''' and the value '''"at work"'''</li>
 +
<li>At prompt count 1 on '''"b"''' we did an alert to change for the key ''''john''''</li>
 +
<li>At prompt count 2 on '''"b"''' we drop into a receive for a Msg which we will print on receipt</li>
 +
<li>At prompt count 2 on '''"a"''' we do a store location, which changes the location for ''''john'''' to '''"at home"''' and causes '''"b"''' to exit
 +
the receive clause printing the message: Msg: {location_changed,{john,"at home"}}. Subscription works!</li>
 +
<li>Finally at prompt count 3 on "a" we prove that fetch_location works.</li>
 +
</ol>
 +
 +
You can download the code for this tutorial [http://www.erlware.org/fos_erlware/priv/downloads/location_server.tgz here] (link currently dead)
 +
That's it, happy Erlanging.
 +
 +
[[Category:HowTo]]

Revision as of 19:05, 14 December 2009

Contents

Author

Martin Logan

Building an OTP Application Overview

Erlang is a clean, simple language, and it is easy to learn. That's Erlang, now lets talk OTP. The learning curve goes up significantly when getting into OTP. Questions arise, such as; how do I start an application, what is the function of a supervisor, and how do I make use of gen_server? That is just the beginning it gets far more confusing... "what is a .app file, what is a release, and can someone please tell me what .script and .boot files do?"

This tutorial will walk you through creating a new OTP application called location_server. This application will store the location of clients, allow others to query for that location from connected node, and allow clients to subscribe to notifications that are sent in the event of a change of location. Over the course of creating the location_server I will answer all of the above questions on OTP applications and quite a few more besides.

Part One: Building the Server

The following instructions may be inaccurate, as otp_base has moved to google code: http://code.google.com/p/otp-base/.

You will need to download the reference build system for this tutorial, this can be found at www.erlware.org under downloads otp_base-<vsn> Once you have the code on your local Unix/Linux machine extract the build system.

> tar -xzfv otp_base-R1.tgz
> cd otp
> ls
build  lib  licence.txt  Makefile  README  release tools

The first thing we are going to do is make an initial build of our build system, this is done by typing "make" in the otp directory. Note: if you have Erlang installed in any place other than /usr/local/lib/erlang then you can either create a symlink there pointing to your install, or you can set the environment variable ERL_RUN_TOP to point to your installation (make sure you use an absolute path).

The second thing to do is create a skeleton for the application. Once we do I will dissect that skeleton and explain the meaning of all its parts. To create the location_server application skeleton we use the "appgen" utility.

> cd tools/utilities
> ./appgen location_server ls
> cd -

We have just created an application under lib/location_server and a release under release/location_server_rel. We will now cd into lib/location_server and see what we have made.

> cd lib/location_server
> ls
include  Makefile  src  vsn.mk

The include directory contains a file called location_service.hrl. This is the place we will put all our shared macro and record definitions. The src directory contains all of our Erlang (.erl) files. The vsn.mk file contains the make variable LOCATION_SERVER_VSN=1.0 which is the initial version of our application (feel free to change this). This version plays a role in how releases are handled, more on this later.

> cd src
> ls
location_server.app.src    location_server.erl  ls_sup.erl
location_server.appup.src  ls_server.erl        Makefile

The src directory contains a file called location_server.erl. Notice that this Erlang source file has the same name as our application. By convention this file contains the API for our application. This file also contains the -behaviour(application) directive. This means that this file can be used as the entry point for the OTP application structure.

%%%-------------------------------------------------------------------
%%% Author  : martinjlogan <martinjlogan@martinjlogan.com>
%%% @doc The entry point into our application.
%%% @end
%%%-------------------------------------------------------------------
-module(location_server).

-behaviour(application).
%%--------------------------------------------------------------------
%% Include files
%%--------------------------------------------------------------------
-include("location_server.hrl").

%%--------------------------------------------------------------------
%% External exports
%%--------------------------------------------------------------------
-export([
     start/2,
     shutdown/0,
     stop/1
     ]).

%%====================================================================
%% External functions
%%====================================================================
%%--------------------------------------------------------------------
%% @doc The starting point for an erlang application.
%% @spec start(Type, StartArgs) -> {ok, Pid} | {ok, Pid, State} | {error, Reason}
%% @end
%%--------------------------------------------------------------------
start(Type, StartArgs) ->
    case ls_sup:start_link(StartArgs) of
    {ok, Pid} ->
        {ok, Pid};
    Error ->
        Error
    end.

%%--------------------------------------------------------------------
%% @doc Called to shudown the auction_server application.
%% @spec shutdown() -> ok
%% @end
%%--------------------------------------------------------------------
shutdown() ->
    application:stop(location_server).

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

%%--------------------------------------------------------------------
%% Called upon the termination of an application.
%%--------------------------------------------------------------------
stop(State) ->
    ok.

The start/2 function is a callback function used by the OTP application system to start applications specified by the OTP release system. This function must take two arguments (you can read the docs on supervision for more info on the args) and it must return {ok, Pid} or {ok, Pid, State}, the former being more typical, for a successful application startup. Notice that the start/2 function calls ls_sup:start_link/1. ls_sup.erl contains our top level supervisor. Notice the prefix "ls" in the name ls_sup it is the first two letters of the two words that make up our application name; location_server. This prefix is a convention and it is used in lieu of a package structure in Erlang to avoid name conflicts within a release. You can use anything for your prefix, I only offer the first letter of each word in your application name as a suggestion.

The top level supervisor starts all the workers and lower level supervisors that our application will require. It specifies a strategy detailing how its processes will be restarted when they crash. This a key concept in Erlang and I suggest reading the design principles on supervision located at www.erlang.org.

%%%-------------------------------------------------------------------
%%% Author  : martinjlogan <martinjlogan@martinjlogan.com>
%%% @doc The top level supervisor for our application.
%%% @end
%%%-------------------------------------------------------------------
-module(ls_sup).

-behaviour(supervisor).

%%--------------------------------------------------------------------
%% External exports
%%--------------------------------------------------------------------
-export([start_link/1]).

%%--------------------------------------------------------------------
%% Internal exports
%%--------------------------------------------------------------------
-export([init/1]).

%%--------------------------------------------------------------------
%% Macros
%%--------------------------------------------------------------------
-define(SERVER, ?MODULE).

%%====================================================================
%% External functions
%%====================================================================
%%--------------------------------------------------------------------
%% @doc Starts the supervisor.
%% @spec start_link(StartArgs) -> {ok, pid()} | Error
%% @end
%%--------------------------------------------------------------------
start_link(StartArgs) ->
    supervisor:start_link({local, ?SERVER}, ?MODULE, []).

%%====================================================================
%% Server functions
%%====================================================================
%%--------------------------------------------------------------------
%% Func: init/1
%% Returns: {ok,  {SupFlags,  [ChildSpec]}} |
%%          ignore                          |
%%          {error, Reason}
%%--------------------------------------------------------------------
init([]) ->
    RestartStrategy    = one_for_one,
    MaxRestarts        = 1000,
    MaxTimeBetRestarts = 3600,

    SupFlags = {RestartStrategy, MaxRestarts, MaxTimeBetRestarts},

    ChildSpecs =
    [
     {ls_server,
      {ls_server, start_link, []},
      permanent,
      1000,
      worker,
      [ls_server]}
     ],
    {ok,{SupFlags, ChildSpecs}}.

This file exports two functions start_link/1 and init/1. These are both functions used to conform to the supervisor behaviour, note the -behaviour(supervisor) directive which is present at the top of the file. Naming a function start_link is a convention in OTP that means the function will spawn a process and that the caller of the function, the parent process, will be linked (see link/1) to the child process. In this case the OTP application structure will be linked to the supervisor process.

The init/1 function is where the true guts of the supervisor are. Remember that you should never put any program logic in your supervisor, it should be as simple as possible and never fail, the fate of your application depends on this. Lets break our super down:

RestartStrategy    = one_for_one,
MaxRestarts        = 1000,
MaxTimeBetRestarts = 3600,

The RestartStrategy one_for_one means that for each process that dies simply restart that process without affecting any of the others. There are two other restart strategies, one_for_all and rest_for_one, you can read about these in the supervisor documentation. MaxRestarts is the maximum number of restarts allowed within MaxTimeBetweenRestarts seconds.

{ls_server,
  {ls_server, start_link, []},
   permanent,
   1000,
   worker,
   [ls_server]}

This supervisor has one ChildSpec which means it will start exactly one process. Going in order down the data structure ls_server is the name of the process to be started, {ls_server, start_link, []} is the module function and args that is called to start the child, permanent refers to the fact that the process should never be allowed to die without being restarted (there are other options, see the docs on supervision at erlang.org), 1000 is the time the process should be given to shutdown before being brutally killed, worker means the child is not a supervisor, [ls_server] is a list of module dependencies of the child.

The supervisor callback module function init/1 returns all of this information and the supervisor behaviour takes care of starting all the children in the order that they were specified in the child specs.

Now we move on to the ls_server.erl file. This file exhibits the behaviour gen_server. This means that it follows the callback prescription for that behaviour and exports the following callback functions:

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

Additionally we are also going to create the following external functions:

%%--------------------------------------------------------------------
%% External exports
%%--------------------------------------------------------------------
-export([
     start_link/0,
     stop/0,
     store_location/3,
     fetch_location/2,
     alert_to_change/2
     ]).

Lets get started. It is important to document all of your external functions. The OTP Base build system supports "edoc" with the "make docs" target which will automatically go through and compile all of your documentation and place it in your application directory under the directory doc (location_server/doc). On to our functions:

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

This function starts the server, registers it locally as ?SERVER (see source for -define(SERVER, ?MODULE).) and indicates to the gen_server behaviour that the current module exports the needed callback functions.

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

Stop is simple, but notice the use of gen_server:cast/2. This function is about the same as using the "!" operator. It is used to send an asynchronous message to a gen server. In this case we send the atom 'stop' to a locally registered process identified by the macro ?SERVER.

%%--------------------------------------------------------------------
%% @doc Stores the location of a client.
%% <pre>
%% Types:
%%  Node = node()
%%  Id = atom()
%%  Location = string()
%% </pre>
%% @spec store_location(Node, Id, Location) -> ok
%% @end
%%--------------------------------------------------------------------
store_location(Node, Id, Location) when is_atom(Id), is_list(Location) ->
    gen_server:cast({?SERVER, Node}, {store_location, {Id, Location}}).

store_location/3 is a very simple function but there is a lot for us to pay attention to here. Notice that we use the guard expressions, is_atom/1, and is_list/1. We are making sure that the types of the variables passed in are correct. This is an example of not trusting our borders. We check the types here so that we will never again have to check them. Did you notice that the name of the function is the same as the "tag" on the message we are sending? No? I will explain;

gen_server:cast({?SERVER, Node}, {store_location, {Id, Location}}).

store_location in the line above is our "tag" and will differentiate this message from others that this server understands (its alphabet). {Id, Location} is the payload that we are sending with the tag store_location. Messages sent in the gen_server should follow this convention of {Tag, Value}. There is an even more important thing to notice about the line above. Notice that the first argument to this function is {?SERVER, Node} instead of just ?SERVER as we saw in cast. This is the case because we want to be able to contact a process on any node in the network not just our own. To message a remotely registered process we can use the syntax {RegisteredName, Node} where node can be any node in the network including the local node.

%%--------------------------------------------------------------------
%% @doc Fetches the location of a client by its Id.
%% <pre>
%% Types:
%%  Node = node()
%%  Id = atom()
%% </pre>
%% @spec fetch_location(Node, Id) -> {ok, Location} | {error, Reason} | EXIT
%% @end
%%--------------------------------------------------------------------
fetch_location(Node, Id) ->
    gen_server:call({?SERVER, Node}, {fetch_location, Id}).

The things to notice here is that we no longer use guards, we don't care, this function will not change the state of our server and if it fails it does not matter much to us. Most importantly notice the use of gen_server:call/2. This function makes a synchronous (blocking) call to a gen_server.

%%--------------------------------------------------------------------
%% @doc Subscribes the caller to notifications of location change for
%% the given Id. When a location changes the caller of this function
%% will be sent a message of the form {location_changed, {Id, NewLocation}}.
%% <pre>
%% Types:
%%  Node = node()
%%  Id = atom()
%% </pre>
%% @spec alert_to_change(Node, Id) -> bool() | EXIT
%% @end
%%--------------------------------------------------------------------
alert_to_change(Node, Id) ->
    gen_server:call({?SERVER, Node}, {alert_to_change, {Id, self()}}).

Now that we have all of our external functions finished lets flesh out our server. One thing that those new to Erlang often get confused by is the idea that these external functions are called in the process space of the calling process but that the messages they generate are received by clauses in the same module but that exist in an entirely different process. It is important to use modules to encapsulate server protocols when possible.

%%====================================================================
%% Server functions
%%====================================================================

%%--------------------------------------------------------------------
%% Function: init/1
%% Description: Initiates the server
%% Returns: {ok, State}          |
%%          {ok, State, Timeout} |
%%          ignore               |
%%          {stop, Reason}
%%--------------------------------------------------------------------
init([]) ->
    error_logger:info_msg("ls_server:init/1 starting~n", []),
    {ok, dict:new()}.

The init function is used to setup process state before the process can be accessed from the outside. Here our state is embodied by a dict. This dict will be used to store key value pairs of the form {Id, {Location, ListOfSubscribers}}. The init function is also logging the fact that the ls_server process is starting up. The location and management of log files will be covered in the second part of this tutorial covering how to create a release for your application.

%%--------------------------------------------------------------------
%% Function: handle_call/3
%% Description: Handling call messages
%% Returns: {reply, Reply, State}          |
%%          {reply, Reply, State, Timeout} |
%%          {noreply, State}               |
%%          {noreply, State, Timeout}      |
%%          {stop, Reason, Reply, State}   | (terminate/2 is called)
%%          {stop, Reason, State}            (terminate/2 is called)
%%--------------------------------------------------------------------
handle_call({fetch_location, Id}, From, State) ->
    case dict:find(Id, State) of
        {ok, {Location, ListOfSubscribers}} -> {reply, {ok, Location}, State};
        error                               -> {reply, {error, no_such_id}, State}
    end;
handle_call({alert_to_change, {Id, Subscriber}}, From, State) ->
    case dict:find(Id, State) of
        {ok, {Location, ListOfSubscribers}} ->
            NewListOfSubscribers = [Subscriber|lists:delete(Subscriber, ListOfSubscribers)],
            NewState             = dict:store(Id, {Location, NewListOfSubscribers}, State),
            {reply, true, NewState};
        error ->
            {reply, false, State}
    end.
%%--------------------------------------------------------------------
%% Function: handle_cast/2
%% Description: Handling cast messages
%% Returns: {noreply, State}          |
%%          {noreply, State, Timeout} |
%%          {stop, Reason, State}            (terminate/2 is called)
%%--------------------------------------------------------------------
handle_cast(stop, State) ->
    {stop, normal, State};
handle_cast({store_location, {Id, Location}}, State) -> % Remember that State is a dict()
    NewState =
        case dict:find(Id, State) of
            {ok, {OldLocation, ListOfSubscribers}} ->
                lists:foreach(fun(Subscriber) -> Subscriber ! {location_changed, {Id, Location}} end, ListOfSubscribers),
                dict:store(Id, {Location, ListOfSubscribers}, State);
            error ->
                dict:store(Id, {Location, []}, State)
        end,
    {noreply, NewState}.

We are done with the server and all the functionality of our location_server application, but we have one last step to take. It is clearly awkward and ugly to have clients of our application calling ls_server:store_location etc... What we need is a well defined external API for clients. The place to put that API is in the module that has the same name as our application, our application behaviour module, location_server.erl.

%%--------------------------------------------------------------------
%% @doc Stores the location of a client.
%% <pre>
%% Types:
%%  Node = node()
%%  Id = atom()
%%  Location = string()
%% </pre>
%% @spec store_location(Node, Id, Location) -> ok
%% @end
%%--------------------------------------------------------------------
store_location(Node, Id, Location) ->
    ls_server:store_location(Node, Id, Location).

%%--------------------------------------------------------------------
%% @doc Fetches the location of a client by its Id.
%% <pre>
%% Types:
%%  Node = node()
%%  Id = atom()
%% </pre>
%% @spec fetch_location(Node, Id) -> {ok, Location} | {error, Reason} | EXIT
%% @end
%%--------------------------------------------------------------------
fetch_location(Node, Id) ->
    ls_server:fetch_location(Node, Id).

%%--------------------------------------------------------------------
%% @doc Subscribes the caller to notifications of location change for
%% the given Id. When a location changes the caller of this function
%% will be sent a message of the form {location_changed, {Id, NewLocation}}.
%% <pre>
%% Types:
%%  Node = node()
%%  Id = atom()
%% </pre>
%% @spec alert_to_change(Node, Id) -> bool() | EXIT
%% @end
%%--------------------------------------------------------------------
alert_to_change(Node, Id) ->
    ls_server:alert_to_change(Node, Id).

Of course the functions must be exported.

%%--------------------------------------------------------------------
%% External exports
%%--------------------------------------------------------------------
-export([
      start/2,
      shutdown/0,
      store_location/3,
      fetch_location/2,
      alert_to_change/2,
      stop/1
     ]).

Now we are done with the coding of location_server, all we have to do is change directories into our application directory, type "make" and "make docs", and wait for a successful source and documentation compile.

> cd ..
> make
> make docs
> ls
doc  ebin  include  Makefile  src  vsn.mk

Notice that there are two new directories in our application root directory "doc" and "ebin". location_server/doc contains html documentation of everything we have done. All of our external functions are present in these docs. Lets explore the ebin directly a little more deeply.

> cd ebin
> ls
location_server.app    location_server.beam  ls_sup.beam
location_server.appup  ls_server.beam

The ebin contains a single .beam byte-code compiled file for each of our .erl source files. The ebin directory also contains a .app and a .appup file. The .appup file is used for release upgrade handling and is beyond the scope of this tutorial. The .app file is essential for creating and installing OTP releases so we will look at it in depth.

{application, location_server,
 [
  % A quick description of the application.
  {description, "An Erlang Application."},

  % The version of the application
  {vsn, "1.0"},

  % All modules used by the application.
  {modules,
   [
    location_server, ls_server, ls_sup
   ]},

  % All of the registered names the application uses. This can be ignored.
  {registered, []},

  % Applications that are to be started prior to this one. This can be ignored
  % leave it alone unless you understand it well and let the .rel files in
  % your release handle this.
  {applications,
   [
    kernel,
    stdlib
   ]},

  % OTP application loader will load, but not start, included apps. Again
  % this can be ignored as well.  To load but not start an application it
  % is easier to include it in the .rel file followed by the atom 'none'
  {included_applications, []},

  % configuration parameters similar to those in the config file specified
  % on the command line. can be fetched with gas:get_env
  {env, []},

  % The Module and Args used to start this application.
  {mod, {location_server, []}}
 ]
}.

The comments in the above code pretty much explain it all. This .app file is created dynamically by the make process from the location_server/src/location_server.app.src file. This means that you should pretty much never have to manually edit this file.

Congratulations you have just created your first beautiful OTP application. Now lets move on to the second part of our tutorial and create a release in order to run and deploy our OTP application.

Part Two: Crafting a Release

From the otp directory:

> cd release/location_server_rel
> ls
location_server_rel.config.src  Makefile  yaws.conf.src
location_server_rel.rel.src     vsn.mk

This directory contains a vsn.mk file which serves to store the version string for this release. It contains a yaws.conf.src file which will be used to generate a yaws.conf file should you choose to include the yaws web server in your release. The two files we are going to concentrate on right now are the location_server_rel.config.src file and the location_server.rel.src file. The .config.src file is what make will translate into a .config file. This file includes all of the configuration the different applications in our release require to do things such as truncate and rotate our log files. The .rel.src file is what the make process will turn into a .rel file which will inturn be used to create our .script and .boot files, the files that tell the Erlang system what code to load when starting up. Here is our .rel.src file.

{release,
 {"location_server_rel", "%REL_VSN%"},
 erts,
 [
  kernel,
  stdlib,
  sasl,
  fslib,
  gas,
  location_server
 ]
}.

This file specifies the name of our release followed by a version string that will be filled in when we do a make with the version string present in our vsn.mk file. Following that we specify that we want erts to be present in our release and the kernel, stdlib, sasl, fslib, gas, and finally our own custom application the location_server. For more details on fslib and gas, the two applications that together provide configuration and log rotation and truncation for our release you can go to erlware.org and see full documentation about them. Alternatively you can cd to either otp/lib/fslib or otp/lib/gas and run "make docs" and just generate all of the docs locally. When we run make this rel.src file will be turned into an actual .rel file which contains all the version numbers associated with the applications in the .rel.src file. For more info on the .rel.src structure take a look at the docs for the fslib file fs_boot_smithe.

Time to build and run our release.

> make
> ls
local                           location_server_rel.rel          Makefile
location_server_rel             location_server_rel.rel.src      vsn.mk
location_server_rel.boot        location_server_rel.rel.src.tmp  yaws.conf.src
location_server_rel.config.src  location_server_rel.script

Quite a number of files and two directories have been generated. We can see a .rel file, a .script file, and a .boot file. Take a look at the contents of each, well, the .boot file might not prove to be too interesting because it is just a binary version of the .script file. Pay attention to the two directories that have been created; local and location_server_rel. location_server_rel is a staging area for installation and production releases of our application, for more info reference OTP Base documentation at erlware.org. We are going to focus on the local directory here. This directory allows us to run our release interactively. If we wanted to run it as a daemon it would be easy enough to invoke local/location_server_rel.sh with the -detached flag to accomplish that. For now we are going to cd into local and just run our application as is.

> cd local
> ./location_server_rel.sh
Erlang (BEAM) emulator version 5.4.12 [source] [hipe]

INFO REPORT 14-Oct-2006::00:18:33
gas_sup:init

INFO REPORT 14-Oct-2006::00:18:33
ls_server:init/1 starting

Eshell V5.4.12  (abort with ^G)
(martinjlogan_location_server_rel@core.martinjlogan.com)1>
We have successfully started the location_server release. Can you see where the log message we placed in ls_server to indicate its having reached the init/1 function has shown up on the console?  That is not the only place our message shows up:

<code caption="Code listing 2.4">
> ls log/1.0/
err_log  sasl_log

The err_log file contains all the logs that get printed via the sasl error_logger with info_msg or error_msg. The other log, sasl_log, is where you will find all the sasl error reports. If your release crashes the first place to look is the sasl_log. By default the build system has wired in the use of fs_elwrap_h via the G.A.S application that is part of your release in order to truncate and rotate log-files. This prevents the log files from growing indefinitely. To configure or remove this functionality you can use the location_server.config file located in the "local" directory.


{gas,
  [
   % Tell GAS to start elwrap.
   {mod_specs, [{elwrap, {fs_elwrap_h, start_link}}]},

   % elwrap config.
   {err_log, "/home/martinjlogan/work/otp/release/location_server_rel/local/log/1.0/err_log"},
   {err_log_wrap_info, {{err,5000000,10},{sasl,5000000,10}}},
   {err_log_tty, true} % Log to the screen
   ]},

The mod_specs inform the GAS application as to how and what services to start up for a release. Following that are the actual config tuples elwrap uses to do its job. Starting with the location of the err_log and followed by the wrap specs which indicate for both the sasl and error logs how large a file can get before it is rotated out and just how many files there may be for a given logger before old files start getting deleted. The last bit of config toggles whether or not the error logger writes to the screen.

Part Three: Testing What We Have Built

To test our project we are going to start 3 separate Erlang named nodes. Open three terminal windows and start three nodes

  1. is the location server as started by local/location_server.sh
  2. is started with erl -name a -pz <path to location_server ebin>
  3. is started with erl -name b -pz <path to location_server ebin>

The -pz option on both tells the interpreter to add the specified path to the code loader search path. This means that we will have the location_server interfaces available for our use in node a and b. (This loading of code without starting an application can be accomplished by adding the tuple 'none' after an included application in the .rel.src file.) So now we have three nodes a@hostname, b@hostname, and username_location_server@hostname. To connect our nodes execute net_adm:ping('a@hostname') from the other two nodes. After this is complete running nodes() on any of the nodes should yield a complete list of the other nodes in our cloud. Run the following commands on your nodes. Make sure you pay attention to the node names in the following example because the commands are not all run on the same node.

(a@hostname)1> location_server:store_location('username_location_server_rel@hostname', john, "at work").
ok

(b@hostname)1> location_server:alert_to_change('username_location_server_rel@hostname', john).
true
(b@hostname)2> receive Msg -> io:format("Msg: ~p~n", [Msg]) end.

(a@hostname)2> location_server:store_location('username_location_server_rel@hostname', john, "at home").
ok

(b@hostname)2>
Msg: {location_changed,{john,"at home"}}
ok

(a@hostname)3> location_server:fetch_location('username_location_server_rel@hostname', john).
{ok,"at home"}

Here is what happened above, we started a location server as the node 'username_location_server_rel@hostname' via the location_server_rel.sh start-up script. Then we started two nodes a@hostname and b@hostname. We then followed these steps:

  1. At prompt count 1 on "a" we did a store location with the key 'john' and the value "at work"
  2. At prompt count 1 on "b" we did an alert to change for the key 'john'
  3. At prompt count 2 on "b" we drop into a receive for a Msg which we will print on receipt
  4. At prompt count 2 on "a" we do a store location, which changes the location for 'john' to "at home" and causes "b" to exit the receive clause printing the message: Msg: {location_changed,{john,"at home"}}. Subscription works!
  5. Finally at prompt count 3 on "a" we prove that fetch_location works.

You can download the code for this tutorial here (link currently dead) That's it, happy Erlanging.